Help assigning var inside anonymous function in macro

Hey!,

I’m defining a macro that is expected to be called with a variable (provided by the user from their scope) and assign to it. I was expecting the anonymous function generated by the code bellow to be able to assign some value into this variable. I was thinking that since I’m unquoting the var explicitly that would make it unambiguous what variable I’m referring to (the user provided one) and not a new variable inside a new function scope.

May you help me get this right?

Thanks, :)`

defmodule FooTest do
  use ExUnit.Case

  defmacro foo(var) do
    quote do
      fn value ->
        old = unquote(var)
        unquote(var) = value
        old
      end
    end
  end

  test "change the given var" do
    x = 1
    assert 1 == foo(x).(99)
    assert x == 99  # Fails here, as x is still 1
  end
end

Edit: Added another test without a macro, but in this case I understand the x inside the anonymous function might actually be fresh-variable on the anon-fun-scope. Again, I’d expect the macro case to work as I’m not talking about a new x but the original user provided one.

  test "change the given var" do
    x = 1
    assert 1 == (fn y -> x = y; x end).(99)
    assert x == 99 # also fails, but that's ok.
  end

I’m on the bus right now and only have my mobile, but if I understand you correctly it’s var!/2 you are searching for.

I can’t give you an example of how to use it, as I’m on the bus.

On the other hand side you should try to redefine your requirements and find a way to do your job without var!/2.

Hey @NobbZ

Oh yes, I already know about var!/2 and know it can be used to affect the context, like:

  defmacro bar() do
    quote do
      var!(x) = 99
    end
  end

  test "var!" do
    x = 22
    bar()
    assert x == 99 # ok!
  end

But in my original foo macro it’s not what I want to use. I want the foo macro to take the variable it should work with, instead of relying on var!/2 (like bar does).

The following for example works:

  defmacro baz(var) do
    quote do
      unquote(var) = 99
    end
  end

  test "baz works" do
    x = 22
    baz(x)
    assert x == 99
  end

So, just like foo, baz takes the variable that will be assigned to. The only difference between foo (which is the one I really want) and baz is that foo creates an anonymous function, and the variable is unquoted for assignment inside of it. So I guess it has to be something definitely related to anon-functions. It’s just that I’d expect my foo macro to just work, as I’m not introducing (nor want) a new variable, I’m just trying to use the one provided to me (like in baz).

What you want (with the anonymous function) cannot work, because data is immutable in Elixir. The function can rebind the variable in its own scope to something else but outside the function the variable value is still unchanged. You cannot change it from inside the function.

1 Like

So, investigating it further, I’ve just wrapped foo (which is now a function) in bla just to inspect the whole generated code. Looking at the output, the variable inside the anon-function of foo being set is just the one given to it by bla.

But I’m sure I’m missing something obvious and just need some sleep haha. No clue.

defmodule FooTest do
  use ExUnit.Case

  def foo(var) do
    quote do
      fn value ->
        old = unquote(var)
        unquote(var) = value
        old
      end
    end
  end

  defmacro bla() do
    # No one should need to set the variable counter
    # but am doing just to be explicit and look
    # by inspecting if it's the same `x` or
    # another variable is being introduced.
    x = {:x, [counter: 1], __MODULE__}
    code = quote do
      unquote(x) = 1
      assert 1 == unquote(foo(x)).(99)
      assert unquote(x) == 99
    end
    IO.inspect(code, label: :code)
    code
  end

  test "macro change the given var" do
    bla()
  end

end

And here’s the output:
See that the {:x, [counter: 1], FooTest} variable is correctly being used in the anonymous function inside foo.

code: {:__block__, [],
 [
   {:=, [], [{:x, [counter: 1], FooTest}, 1]},
   {:assert, [context: FooTest, import: ExUnit.Assertions],
    [
      {:==, [context: FooTest, import: Kernel],
       [
         1,
         {{:., [],
           [
             {:fn, [],
              [
                {:->, [],
                 [
                   [{:value, [], FooTest}],
                   {:__block__, [],
                    [
                      {:=, [],
                       [{:old, [], FooTest}, {:x, [counter: 1], FooTest}]},
                      {:=, [],
                       [{:x, [counter: 1], FooTest}, {:value, [], FooTest}]},
                      {:old, [], FooTest}
                    ]}
                 ]}
              ]}
           ]}, [], 'c'}
       ]}
    ]},
   {:assert, [context: FooTest, import: ExUnit.Assertions],
    [
      {:==, [context: FooTest, import: Kernel],
       [{:x, [counter: 1], FooTest}, 99]}
    ]}
 ]}


  1) test macro change the given var (FooTest)
     test/callbag_test.exs:53
     Assertion with == failed
     code:  assert x == 99
     left:  1
     right: 99
     stacktrace:
       test/callbag_test.exs:54: (test)

Oh, so you can re-bind a variable only on it’s own function scope. So what’s happening in my code, am I introducing just another x (with same module context FooTest, and even the same :counter) but they are actually different variables?

If I’m reading this right, what your code boils down to after the macro expansion is

x = 1
(fn var -> var = 2 end).(x)
assert x == 2 # Crashes

So in your function you are rebinding the variable var to something but when the function is over that context will be lost and only the returned value will remain. So you can see that it’s not possible to change the binding of x from inside the function.

No, actually it’s:

x = 1
(fn value ->
  old = x
  x = value # I was expecting since I'm unquoting the original var,
             # and since in the AST the variable has the correct context
             # to be able to assign to it.
  old
).(99)
x == 99 # crashes

But even when the inspected code prints that my x variable is the same in the whole AST, it’s not actually being the same at execution time. So I guess, just like you said, it’s just another x inside the anonymous function. (Even if I set the module context and even the variable counter, haha)

Hm…

1 Like

Yeah, this has nothing to do with macros. It’s just the way var scope works (every function has its own scope). So even if you can re-assign a variable, it’s only possible inside the same function scope. An anonymous function, even when it creates a closure, can read from them, but if some var is assigned (like I was trying to do) it’s a completely new variable inside the inner function scope.

1 Like

So when you write in Elixir:

  a = 1
  a = 2

I’m just guessing (havent looked at the compiler) that they are actually two different erlang variables (as in erlang you cant re-assign to previously defined variables. It’s just that the compiler keeps track of which is the last a for subsequent expressions I guess.

2 Likes

So, all the code I wrote was just meta-code for the examples in the initial post, haha. Just trying to realize what was happening with that x variable.

Thanks both @NobbZ, @Nicd for reading and replying to my nonsense.

Time for me to get some sleep, otherwise I cant get some pretty obvious stuff :smiley:

1 Like

And found this document,

http://elixir-lang.readthedocs.io/en/latest/technical/scoping.html#scope-nesting-and-shadowing

Any variable in a nested scope whose name coincides with a variable from the surrounding scope will shadow that outer variable. In other words, the variable inside the nested scope temporarily hides the variable from the surrounding scope, but does not affect it in any way.

3 Likes

Yeah for note this gets compiled into something similar to this at the erlang level:

a$0 = 1
a$1 = 2
2 Likes