hylang / hy

A dialect of Lisp that's embedded in Python
http://hylang.org
Other
4.84k stars 367 forks source link

`cl-letf` in Hy #1660

Closed brandonwillard closed 6 years ago

brandonwillard commented 6 years ago

After a macro expansion, I ended up in a situation (this one, to be exact) like the following:

=> (setv some-dict {"some-key" None})
=> (require [hy.contrib.walk [*]])
=> (let [(get some-dict "some-key") 2]
... (print some-dict))
  File "<input>", line 1, column 7

  (let [(get some-dict "some-key") 2]
        ^------------------------^
HyMacroExpansionError: bind targets must be symbols

For this situation to work out, it seems like let would have to behave like Emacs' cl-letf.

A very related functionality — namely, generalized variables in Emacs — does appear to work in Hy:

=> (setv (get some-dict "some-key") 1)
=> (print some-dict)
{'some-key': 1}

That said, is there any place/interest in having an extended let that allows this (when reasonable) or a separate letf?

gilch commented 6 years ago

The purpose of Hy's let is to create lexical variables. I'm not certain from your example what you want, but I think you're asking for dynamic variables hylang/hyrule#51, which is the default let behavior in Emacs.

Python does this kind of thing with context managers and the with statement, which Hy can also use. If you want to temporarily mutate a dict, then put it back at the end of the block, you want with, not let. You'd have to write the code to both do and undo your assignment, but it will be reusable.

Another option might be to use a collections.ChainMap in combination with let, to preserve the underlying dict.

=> (let [some-dict (ChainMap {} some-dict)]
...  (setv (get some-dict "some-key") 2)
...  (print some-dict)
...  (print (get some-dict "some-key")))

ChainMap({'some-key': 2}, {'some-key': None})
2
=> (print some-dict)
{'some-key': None}
brandonwillard commented 6 years ago

I didn't think about it in terms of broader variable scoping and/or binding, but I suppose that could be part of it.

However, does that still apply in a situation like the following?

> (let [some-dict {"some-key" None}
...     (get some-dict "some-key") 1]
... (print some-dict))
  File "<input>", line 2, column 6

       (get some-dict "some-key") 1]
       ^------------------------^
HyMacroExpansionError: bind targets must be symbols

Another way to state the functionality I'm describing: alter let to allow any valid assignment target as a target in its bindings.

gilch commented 6 years ago

If you want to permanently update a target, use setv. And recall that macros can return a do block (named from Clojure's do, which is like the Common Lisp PROGN or Scheme begin).

If you want to update a target for the duration of a block (and then put it back), use with and a patching context manager.

=> (import [unittest.mock [patch]])
None
=> (setv a-dict {"key" None})
None
=> (with [(patch.dict a-dict {"key" 2})]
...  (print a-dict))

{'key': 2}
=> (print a-dict)
{'key': None}

See also: patch.object, patch.multiple, and contextvars. This is dynamic scope.

If you want some kind of lexically-scoped update of a generalized target, then I don't understand what that means.

If you want some kind of destructuring, let doesn't support that, but see hylang/hy#1328. I think a let=: would be possible.

alphapapa commented 6 years ago

Maybe what we need is a hy.cl library. It'd be nice to be able to use letf, flet, labels, etc.

spiderbit commented 6 years ago

I wonder if that problem is no reason to fork? It would be hard to change the muscle-memory from (let (...) foo) to (with [x 5] foo) or something like that. But I think not even that would work.

What is the goal of hy, it should be a as much as possible lisp with a ptyhon backend?

I thought it's just not possible that python has no block scope so you can't implement a lisp like (let) construct, but I just learned aboutd the del() function.

So you could compile:

(let (foo 5) (bar))

into:

foo = 5
bar()
del (foo)

I just wonder, because without let I can't take a lisp like language serious. I don't want to programm python code with a bit different parens syntax.

gilch commented 6 years ago

A library is not a fork.

Hy should be as much like Python as possible, but homoiconic with macros and expression-oriented like Lisp, with features mostly taken from Clojure, but also Common Lisp.

Python does not have arbitrary block scope, but it does have late-binding lexically-scoped function closures.

del is a statement/special form, not a function. You could certainly write your own macro to do that with del, but that's not how let works, even in Emacs. Think about what happens if you nest them.

let doesn't mean what you think it means. What you're trying to do is dynamic scope. But Hy's let is lexically scoped, like it is in Clojure and Common Lisp, neither of which have a letf. Python doesn't have a separate namespace for function identifiers like Common Lisp and Emacs do. So Hy is more of a Lisp-1, like Scheme and Clojure.

refi64 commented 6 years ago

@spiderbit As already stated above, Hy has a let implementation that mostly does what you want. Using del has been discussed before but rejected because it interferes with closures.

spiderbit commented 6 years ago

that's good to hear, why do I not see it then in the documentation homepage?

neither there: http://docs.hylang.org/en/stable/language/api.html#built-ins

nor there: http://docs.hylang.org/en/stable/tutorial.html#hy-is-a-lisp-flavored-python

where is it hidden? :D

gilch commented 6 years ago

http://docs.hylang.org/en/master/contrib/walk.html

brandonwillard commented 6 years ago

@gilch, on a somewhat related note, I came across this thread (and this one, too) a while back when searching for letf in CL.

Kodiologist commented 6 years ago

Anyway, I think @gilch's diagnosis of why the idea of letf doesn't really make sense (at least in Python and with a lexical let) is spot on, so I'm closing this issue.