Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove redundant keywords #240

Closed
joehakimrahme opened this issue Jul 10, 2013 · 38 comments · Fixed by #889
Closed

Remove redundant keywords #240

joehakimrahme opened this issue Jul 10, 2013 · 38 comments · Fixed by #889

Comments

@joehakimrahme
Copy link
Contributor

There are redundant keywords in the current implementation:

  • setf/setv/def
  • lambda/fn
  • for/foreach (?)
  • throw/raise
  • catch/except
  • do/prgn

I believe we should avoid any ambiguity by removing duplication and settling on one of each. It's one of these things that we should worry about now, while we can afford to break the language.

@paultag
Copy link
Member

paultag commented Jul 10, 2013

FWIW, lambda isn't entirely redundant, since it will attempt to use a python lambda if it's able to do so, and only create a function if it needs to, whereas fn always creates a function.

We should clean that up.

def, in my mind, should create a global symbol, a-la Clojure.

In retrospect, not such a great idea, since we do allow (defn) inside (defn). More thought needed.

@joehakimrahme
Copy link
Contributor Author

I added for/foreach, but I'm not sure. Is there anything that for can do that foreach cannot?

@tuturto
Copy link
Contributor

tuturto commented Jul 10, 2013

for can build nested loops neatly: (for [x (range 10) y (range 10)] (print x y))

@joehakimrahme
Copy link
Contributor Author

I added 3 new pairs. I prefer raise and except, because of Python.

@rwtolbert
Copy link
Member

+1 for the comment on nested loops with for

@gilch
Copy link
Member

gilch commented Jun 27, 2015

We also have
defn/defun
defn-alias/defun-alias
first/car
lif/lisp-if (and -not)
rest/cdr

This redundancy also bothers me. If there actually is a meaningful difference in how things are compiled/optimized, (lambda/fn? def/setv?) then it should at least be documented, so we know which one to use when. But even then I'd prefer not to have to choose. Hy should use Clojure forms rather than Common Lisp forms when there are near equivalents to choose between.

In cases the above doesn't apply, I generally prefer the shorter names.

@gilch
Copy link
Member

gilch commented Jul 24, 2015

@algernon Can we also have this one in the "Grand Language Cleanup" milestone?

@algernon
Copy link
Member

The "which one to use" is a funny can of worms. But yes, this belongs to the Grand Language Cleanup.

Last time this came up, one idea was to have a namespace that sets up the aliases. So if someone prefers CL-ish names: (import hy.style.common-lisp)

@algernon algernon added this to the Grand Language Cleanup milestone Jul 25, 2015
@gilch
Copy link
Member

gilch commented Jul 25, 2015

The "which one to use" is a funny can of worms.

sigh
I think Python3, then Clojure, then Common Lisp, in that order, is a good preference heuristic for which to use. In fact, I think it resolves all of them. Hy really does not need to be compatible with Common Lisp (or Clojure for that matter). Hy is its own Lisp dialect, and We're still Python(TM).

Last time this came up, one idea was to have a namespace that sets up the aliases.

Here's an idea for ya, import hy.style will not change the language, but instead print out friendly error messages whenever you attempt to use the wrong Lisp dialect. Like say you start a form with a progn symbol and Hy replies, "you meant to use a do here, didn't you, dear?" That way people will actually learn Hy, and will use a mutually intelligible version of it ;)

@algernon
Copy link
Member

Haha, I like that suggestion.

Oh boy, the possibilities! Tie it in with hydiomatic, and funny warning messages.... mmm. That'd be going a bit too far, though. So perhaps hy.style shouldn't be part of the core language.

@gilch
Copy link
Member

gilch commented Jul 25, 2015

Found some more.
True/true
False/false
None/nil/null

I'll admit to using the easier-to-type aliases almost exclusively. But going by the heuristic, the clear winners are True/False/None.

These have to be reserved words anyway for Python interop, e.g. even if we decided to use nil exclusively to mean None, we still couldn't do (setv None 42), because this isn't compatible with Python None = 42 # error, whereas the reverse, nil = 42 is perfectly valid Python.

Common Lisp programmers unfamiliar with Clojure will be confused by nil != []. This will be less of an issue if we don't call a None a nil.

@gilch
Copy link
Member

gilch commented Aug 11, 2015

OK, we've got most of #880 merged (except the last commit, The -alias functions were moved instead of removed.). Now the following duplicates have been fixed:

raise / throw
except / catch
do / progn
defn / defun
lif / lisp-if and lif-not / lisp-if-not
defn-alias / defun-alias

The remaining duplicates (I know of) are:

True / true
False / false
None / nil / null
first / car
rest / cdr
lambda / fn
def / setv

I'd like to start with the first of the two remaining groups. In #880, @paultag said he's "OK with purging true false null and nil.", and my rationale is just above that. If another core member approves, I can try to put together a pull that purges these from Hy. If not, I'd rather not waste the time. It looks a little more difficult than the last fix.

@refi64
Copy link
Contributor

refi64 commented Aug 11, 2015

I'm -1 for removing true, false, and nil because:

  1. The argument that "users of other Lisps will be confused by the behavior of nil" doesn't matter, since one pretty much always learns Python before Hy.
  2. They look really cool!

Now, I guess one of None aliases could be removed, since maybe 3 is a bit much...

But true and false pretty much don't introduce any confusion, since it's the same word as the uppercase True and False.

@algernon
Copy link
Member

One of nil and null should go, I agree, but not both. The lower-case names aren't confusing, and feel more Hyish to me. Purging them would create more work than what little complexity it removes. It's just not worth it.

So, for the second group, I suggest dropping null, that's neither Pythonic, nor Lispy.

@gilch
Copy link
Member

gilch commented Aug 11, 2015

I should just make a pull purging null, I doubt anyone would object, because it's hardly ever used. The others seem a bit more controversial, as anticipated.

@refi64
Copy link
Contributor

refi64 commented Aug 11, 2015

Yeah, null isn't really used in Python or any Lisp: no objection there.

@gilch
Copy link
Member

gilch commented Aug 12, 2015

This wasn't supposed to get closed yet, can we re-open? Did Github do it automatically again?

@zackmdavis zackmdavis reopened this Aug 12, 2015
@zackmdavis
Copy link
Contributor

Reopened; I agree that the way GitHub apparently closes an issue because a pull request mentioning it was merged (and then attributes the closing to whoever did the merge) is a very questionable "feature."

@algernon
Copy link
Member

The PR had a "close #240" string in its description, GitHub caught that, and thought you want to close it. You have to be careful around that thing, it listens to some keywords and ignores context, and then goes and destroys valid issues violently. Tsk, tsk.

It can be a useful feature too, but one does have to pay attention.

@farhaven
Copy link
Contributor

For setv/def, I'm not sure whether those should be unified. I often do (def foo 'something) at the top level and use (setv bar 'yes) inside functions. If one of the two has to go, I'm in favor of removing def, since (do (def x 1) (def x 2)) feels weirder than (do (setv x 1) (setv x 2)).

@algernon
Copy link
Member

Nothing has to be removed. Some convenience is worth a little extra complexity (though aliases aren't all that complex).

@gilch
Copy link
Member

gilch commented Aug 12, 2015

If one of the two has to go, I'm in favor of removing def

def cannot simply be removed, since it's reserved in Python. I suppose we could rename defn to def though. That way it would do the same thing as in Python, and have the advantage of not confusing Python users.

Another option. We could also make def and setv both do assignments, but behave differently:

>>> x = y = 1
>>> x
1
>>> y
1

Surprisingly, the above works, despite y=1 not counting as an expression with a return value. Python has special grammar rules for assignments. We could support this by redefining def, but keep the current behavior of setv.

=> (def x y 1)
x = y = 1
1
1
=> [x y]
[x, y]
[1, 1]
=> (setv x 2 y 3)
x = 2
y = 2
(x, y)
(2, 3)

I'm not sure if I like this approach though. There may be better ways of getting the chained assignments. Maybe give setv a kwonly argument like this:

=> (setv x y := 1)
1

The presence of the := keyword changes it from pairing to chaining.

Currently, by convention, def is used for global constants, but setv is used for variables. The problem is, this is only a convention, and Python has no way to enforce this. This seems redundant when using caps/earmuffs anyway. We could alter the convention slightly, and make def create globals. So you could use a def inside a function but it would use the global keyword on its variable.

On the other hand, globals are considered harmful. Maybe we shouldn't go out of our way to support them. Renaming defn keeps sounding better and better.

Edit: or we could just remove setv and always use def, even if it feels weird at first, you'll get used to it.

@refi64
Copy link
Contributor

refi64 commented Aug 12, 2015

What about making def force its target to always be a global? I never was too fond of:

global x
x = 1

@refi64
Copy link
Contributor

refi64 commented Aug 12, 2015

That would also align nicely with the current usage.

@paultag
Copy link
Member

paultag commented Aug 12, 2015

That'll break when you're at module level :(
On Aug 12, 2015 12:36 PM, "Ryan Gonzalez" notifications@github.com wrote:

That would also align nicely with the current usage.


Reply to this email directly or view it on GitHub
#240 (comment).

@paultag
Copy link
Member

paultag commented Aug 12, 2015

Also nested defns
On Aug 12, 2015 12:39 PM, "Paul R. Tagliamonte" paultag@gmail.com wrote:

That'll break when you're at module level :(
On Aug 12, 2015 12:36 PM, "Ryan Gonzalez" notifications@github.com
wrote:

That would also align nicely with the current usage.


Reply to this email directly or view it on GitHub
#240 (comment).

@gilch
Copy link
Member

gilch commented Aug 12, 2015

What about making def force its target to always be a global?

I already said that:

We could alter the convention slightly, and make def create globals. So you could use a def inside a function but it would use the global keyword on its variable.

And kind of rejected it:

On the other hand, globals are considered harmful. Maybe we shouldn't go out of our way to support them. Renaming defn keeps sounding better and better.

@refi64
Copy link
Contributor

refi64 commented Aug 12, 2015

Ah, I missed that. :)

@gilch
Copy link
Member

gilch commented Aug 13, 2015

That'll break when you're at module level :(

I thought Python actually allowed global declarations at module level.

@refi64
Copy link
Contributor

refi64 commented Aug 13, 2015

I thought too...

@gilch
Copy link
Member

gilch commented Aug 13, 2015

let may also end up aliasing setv / def, depending on implementation. I'd rather we not call it let if we can't do it properly.

@gilch
Copy link
Member

gilch commented Aug 13, 2015

I propose removing car and cdr.

rest / cdr are not quite the same, but still redundant. (cdr coll) is a macro that expands into a slice like coll[1:]. rest is a partial application of the function islice that does about the same thing as cdr. As a function, rest can be assigned to variables, passed to other functions, etc. As a macro, cdr cannot. Slices also don't work on generators so cdr doesn't work, but islices do, and so does rest. rest will do in almost all cases we currently use cdr.

The main problem with replacing all slices with islices, is that islice will always be a generator, but slices return another instance of the same collection type. Most of the time this isn't a problem. Python's libraries are very good about generally accepting iterables where you might have used a collection before. The main exception is strings, which really need to stay as strings.

If you do run into a case like this, you can still use (cut coll 1) for the same effect.

The case for dropping car in favor of first is even stronger. car is a macro like coll[0], while first is a function like next(islice(coll, 0, None)), which should perhaps be simplified to next(iter(coll)). Either way, you get the first element. Except car doesn't work on generators, and can't be passed as a higher-order function, etc. There is absolutely no reason to keep car except for symmetry with cdr, which we should also drop.

@farhaven
Copy link
Contributor

While we are there, I'd also suggest removing cons. Cons cells don't mesh that well with lists and generators, which are returned/handled by almost everything else.

@algernon
Copy link
Member

I use cons quite extensively in adderall. Without cons, both adderall and hydiomatic would be much harder to implement, and to work with.

@gilch
Copy link
Member

gilch commented Aug 14, 2015

Does cons even make cons cells?

=> (cons 0 [1 2 3])
from hy.core.language import cons
cons(0, [1, 2, 3])
[0, 1, 2, 3]
=> (type (cons 1 None))
from hy.core.language import cons
type(cons(1, None))
<class 'hy.models.expression.HyExpression'>

Seems to mesh with lists just fine.

=> (cons -1 (range 4))
from hy.core.language import cons, range
cons((-1), range(4))
(-1 . range(0, 4))
=> (list (cons -1 (range 4)))
from hy.core.language import cons, range
list(cons((-1), range(4)))
[-1, 0, 1, 2, 3]
=> (type (cons -1 (range 4)))
from hy.core.language import cons, range
type(cons((-1), range(4)))
<class 'hy.models.cons.HyCons'>

Maybe that's a cons cell. It seems to work with generators too.

@algernon
Copy link
Member

=> (cons 1 2)
(1L . 2L)
=> (type (cons 1 2))
<class 'hy.models.cons.HyCons'>

This is what I use extensively in adderall & hydiomatic. This lets me do stuff like:

(run* [q] 
  (prep 
    (== ?expr `(defn foo [params] (something) (something)))
    (== ?expr `(~?fn ~?name ~?params . ~?body))
    (== q ?body)))
;; => [((u'something') (u'something'))]

Unifying on (?foo . ?whatever) means that ?whatever will be the rest of the expression. It'd doable with lists and generators and whatnot, but noticably harder, and expressing the intent wouldn't be so easy, either. With cons cells, this is natural: some head, and a tail. Incredibly useful.

@farhaven
Copy link
Contributor

Okay, I'm convinced :).

@Kodiologist
Copy link
Member

Kodiologist commented Feb 26, 2017

#909 and #911 cover the only remaining cases, so we can close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants