Repository: hylang/hy

Issue:
Make assoc a function instead of a special form
Even though Hy implements `assoc`, somehow it's not a valid function in the local scope:

```
$ hy --spy
hy 0.11.0 using CPython(default) 2.7.10 on Darwin
=> assoc
assoc
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'assoc' is not defined
```

Strange. Let's import `toolz` version:

```
=> (import [toolz.dicttoolz [assoc]])
from toolz.dicttoolz import assoc
=> assoc
assoc
<function assoc at 0x1022af140>
```

Good. Let's try:

```
=> (= (assoc {} "Say" "Hy") {"Say" "Hy"})
{}[u'Say'] = u'Hy'
(None == {u'Say': u'Hy', })
False
=> 
```

Double WTF.
1. Hy's `assoc` implementation is weird since it mutates instead of returning a new dict.
2. I can't shadow with a saner version of `assoc`.

:crying_cat_face: 

make assoc use a gensym
Clojure:

``` Clojure
user=> (defn spam [] (println "Spam!") {})
#'user/spam
user=> (assoc (spam) 1 10, 2 20, 3 30)
Spam!
{1 10, 2 20, 3 30}
```

Hy:

``` Hy
=> (defn spam [] (print "Spam!") {})
=> (assoc (spam) 1 10  2 20  3 30)
Spam!
Spam!
Spam!
```

This is surprising. It could be corrected with a gensym.

Also `assoc` doesn't return the dict as in Clojure. But in Python's standard library, all of the mutating functions and methods I can think of return `None`, so perhaps this was intentional.



Discussion:
Well...
1. If `assoc` returned a new dict, I would be mad. Issue is that Hy <=> Python.
2. I believe this would be partly fixed by #725?

So, unfortunately, it won't fix it, due to the compiler being a bit dumb
about some of this stuff

Namely, if the compiler sees a form with a name, and that name is a
builtin, it'll generate the AST for it. Since `assoc` turns into index
assignment, it'll
always do that, without turning it into an `ast.Call`.

I would be open to a patch removing `assoc` from builtins _entrirely_, and
turn it into a function, though!

@paultag I have a fix "ready" [here](https://github.com/kirbyfan64/hy/commit/7eb1353e09aca6db78223e828b741bbe8b293e01), but it's giving a really weird error:

```
ryan@DevPC-LX:~/stuff/hy$ ./run
hy 0.11.0 using CPython(default) 2.7.6 on Linux
=> (setv x {})
=> (assoc x 2 2)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/media/ryan/stuff/hy/hy/core/language.hy", line 301, in assoc
    (is x None))
NameError: global name 'v' is not defined
=> 
ryan@DevPC-LX:~/stuff/hy$ 
```

This is what `hy2py` outputs for `hy/core/language.hy`:

``` hy
def assoc(tgt, *args):
    if (not is_even(len(args))):
        raise TypeError(u'expected an even number of key-value pairs')
        _hy_anon_var_2 = None
    else:
        _hy_anon_var_2 = None
    it = iter(args)
    for [k, v] in zip(it, it):
        tgt[k] = v
    return tgt
```

It looks like it should work. What's wrong?

for k, v = zip(it, it) ?

> On Jul 14, 2015, at 12:20 PM, Ryan Gonzalez notifications@github.com wrote:
> 
> @paultag I have a fix "ready" here, but it's giving a really weird error:
> 
> ryan@DevPC-LX:~/stuff/hy$ ./run
> hy 0.11.0 using CPython(default) 2.7.6 on Linux
> => (setv x {})
> => (assoc x 2 2)
> Traceback (most recent call last):
>   File "<input>", line 1, in <module>
>   File "/media/ryan/stuff/hy/hy/core/language.hy", line 301, in assoc
>     (is x None))
> NameError: global name 'v' is not defined
> => 
> ryan@DevPC-LX:~/stuff/hy$ 
> 
> This is what hy2py outputs for hy/core/language.hy:
> 
> def assoc(tgt, *args):
> 
> if (not
>  is_even(len(args))):
> 
> raise
>  TypeError(u'expected an even number of key-value pairs')
>         _hy_anon_var_2 = 
> None
> 
> ```
> else:
>     _hy_anon_var_2 = 
> ```
> 
> None
> 
> ```
> it = iter(args)
> ```
> 
> for
>  [k, v] in zip(it, it):
>         tgt[k] = v
>     return tgt
> 
> It looks like it should work. What's wrong?
> 
> —
> Reply to this email directly or view it on GitHub.

Yeah, hunh. What's the hy function look like?
On Jul 14, 2015 12:20 PM, "Ryan Gonzalez" notifications@github.com wrote:

> @paultag https://github.com/paultag I have a fix "ready" here
> https://github.com/kirbyfan64/hy/commit/7eb1353e09aca6db78223e828b741bbe8b293e01,
> but it's giving a really weird error:
> 
> ryan@DevPC-LX:~/stuff/hy$ ./run
> hy 0.11.0 using CPython(default) 2.7.6 on Linux
> => (setv x {})
> => (assoc x 2 2)
> Traceback (most recent call last):
>   File "<input>", line 1, in <module>
>   File "/media/ryan/stuff/hy/hy/core/language.hy", line 301, in assoc
>     (is x None))
> NameError: global name 'v' is not defined
> =>
> ryan@DevPC-LX:~/stuff/hy$
> 
> This is what hy2py outputs for hy/core/language.hy:
> 
> def assoc(tgt, *args):
>     if (not is_even(len(args))):
>         raise TypeError(u'expected an even number of key-value pairs')
>         _hy_anon_var_2 = None
>     else:
>         _hy_anon_var_2 = None
>     it = iter(args)
>     for [k, v] in zip(it, it):
>         tgt[k] = v
>     return tgt
> 
> It looks like it should work. What's wrong?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/hylang/hy/issues/841#issuecomment-121295557.

@paultag https://github.com/kirbyfan64/hy/blob/7eb1353e09aca6db78223e828b741bbe8b293e01/hy/core/language.hy#L40:

``` hy
(defn assoc [tgt &rest args]
  (if (not (even? (len args)))
    (raise (TypeError "expected an even number of key-value pairs")))
  (setv it (iter args))
  (for [[k v] (zip it it)]
    (assoc* tgt k v))
  tgt)
```

The easiest way to accomplish this is probably to simplify `assoc` in the compiler to only accept one pair, then rename to `assoc*`, and then re-implement the multi-pair `assoc` as a macro in terms of `assoc*` with a gensym. The common case of one pair need not expand to use a gensym, unless we want to change `assoc` to return the dict instead of `nil`.

Oh snap, this was open with no PRs when I started working on it last night. I'll just close my PR.

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1406,30 +1406,6 @@ def compile_cut_expression(self, expr):
                             step=step.expr),
             ctx=ast.Load())
 
-    @builds("assoc")
-    @checkargs(min=3, even=False)
-    def compile_assoc_expression(self, expr):
-        expr.pop(0)  # assoc
-        # (assoc foo bar baz)  => foo[bar] = baz
-        target = self.compile(expr.pop(0))
-        ret = target
-        i = iter(expr)
-        for (key, val) in ((self.compile(x), self.compile(y))
-                           for (x, y) in zip(i, i)):
-
-            ret += key + val + ast.Assign(
-                lineno=expr.start_line,
-                col_offset=expr.start_column,
-                targets=[
-                    ast.Subscript(
-                        lineno=expr.start_line,
-                        col_offset=expr.start_column,
-                        value=target.force_expr,
-                        slice=ast.Index(value=key.force_expr),
-                        ctx=ast.Store())],
-                value=val.force_expr)
-        return ret
-
     @builds("with_decorator")
     @checkargs(min=1)
     def compile_decorate_expression(self, expr):


Test patch:
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -628,12 +628,27 @@
   (assoc vals "two" "three")
   (assert (= (get vals "two") "three")))
 
+
 (defn test-multiassoc []
   "NATIVE: test assoc multiple values"
   (setv vals {"one" "two"})
   (assoc vals "two" "three" "four" "five")
   (assert (and (= (get vals "two") "three") (= (get vals "four") "five") (= (get vals "one") "two"))))
 
+
+(defn test-assoc-eval-lvalue-once []
+  ;; https://github.com/hylang/hy/issues/1068
+  "`assoc` only evaluates its lvalue once"
+  (setv counter [])
+  (setv d {})
+  (defn f []
+    (.append counter 1)
+    d)
+  (assoc (f)  "a" 1  "b" 2  "c" 3)
+  (assert (= d {"a" 1  "b" 2  "c" 3}))
+  (assert (= counter [1])))
+
+
 (defn test-pass []
   "NATIVE: Test pass worksish"
   (if True (do) (do))