As for me it's strange variable context issue inside macros.

63 views
Skip to first unread message

Alex Zatvorskiy

unread,
Oct 9, 2012, 6:16:22 AM10/9/12
to elixir-l...@googlegroups.com
Hi there. I've got strange behavior with var!.

Let's consider a few macros definition:


defmacro remove_field(name) do
  quote do: var!(body) = :proplists.delete(to_b(unquote(name)), var!(body))
end


defmacro remove_fields(names) do
  quote do: lc f inlist unquote(names), do: remove_field(f)
end

def to_b(name) when is_atom(name), do: atom_to_binary(name)
def to_b(name), do: name


their ExUnit:

import CouchNormalizer.Scenario

@fixture [{"field", :v}, {"field_2", :v2}, {"field_3", :v3}]

test :remove_field do
  body = @fixture

  remove_field(:unknown)
  assert @fixture == body

  remove_field(:field)
  assert [{"field_2", :v2}, {"field_3", :v3}] == body

  remove_field("field_2")
  assert [{"field_3", :v3}] == body
end

test :remove_fields do
  body = @fixture

  remove_fields [:unknown, :unknown]
  assert @fixture == body

  remove_fields [:field_2, :field]
  assert [{"field_3", :v3}] == body
end


As a result: 
     - :remove_field passed, 
     - :remove_fields failed with

          2) test_remove_fields
            ** (ExUnit.AssertionError) Expected [{"field_3",:v3}] to be equal to (==) [{"field",:v},{"field_2",:v2},{"field_3",:v3}]
            ...

What the catch? Probably I don't understand some special cases for 'var!'

José Valim

unread,
Oct 9, 2012, 7:55:36 AM10/9/12
to elixir-l...@googlegroups.com
You probably want to move the loop to outside of quote in remove_fields:

    lc name in names, do: quote(do: remove_field(unquote(name)))

Otherwise you are passing a variable named f to remove_field which will be defined instead of the ones desired.

José Valim
Skype: jv.ptec
Founder and Lead Developer
Message has been deleted

Alex Zatvorskiy

unread,
Oct 9, 2012, 11:27:47 AM10/9/12
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
It seems like same: 

defmacro remove_fields(names) do
  lc name inlist names, do: quote(do: remove_field(unquote(name)))
end

test :remove_fields do
  body = @fixture

  remove_fields [:unknown, :unknown]
  assert @fixture == body

  remove_fields([:field, :field_2])
  assert [{"field_3", :v3}] == body
end


     2) test_remove_fields (CouchNormalizer.ScenarioTest)
       ** (ExUnit.AssertionError) Expected [{"field_3",:v3}] to be equal to (==) [{"field_2",:v2},{"field_3",:v3}]

José Valim

unread,
Oct 9, 2012, 12:23:50 PM10/9/12
to elixir-l...@googlegroups.com
Is this code open sourced somewhere that I can go and run the tests myself? :)



José Valim
Skype: jv.ptec
Founder and Lead Developer



Alex Zatvorskiy

unread,
Oct 10, 2012, 12:45:28 PM10/10/12
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br

José Valim

unread,
Oct 10, 2012, 1:30:43 PM10/10/12
to elixir-l...@googlegroups.com
So, I have identified the issue. It happens to how Erlang/Elixir variables work.

Whenever you call any function or create a data structure, you can imagine that Elixir takes a snapshot of your variables and pass it to the call site. That said, any variable changes in the call site, won't be propagated to the other parts of the call site. This is very easy to see in iex:

    [x = 1, y = x + 2]

This is going to fail because x is not defined in the second element. You would have to define x outside the list.

This means that the current code for remove_fields is returning the following:

    body = ...
    [body = :proplists.delete(..., body), body = :proplists.delete(..., body)]

If we properly version the variables, we can see it is translated to:

    body1 = ...
    [body2 = :proplists.delete(..., body1), body3 = :proplists.delete(..., body1)]

Which explains your results. body3 is not being built on function of body2.
How to fix this? We need to convert the array into a block expression. Namely, we want:

    body = ...
    body = :proplists.delete(..., body)
    body = :proplists.delete(..., body)

I have just pushed a fix to Elixir master that allows you to achieve it as:

    defmacro remove_fields(names) do
      removers = lc name inlist names, do: quote(do: remove_field(unquote(name)))
      quote do: unquote_splicing(removers)
    end

unquote_splicing is how you splice many expressions into a specific place. In this case, we are splicing them into the body.

José Valim
Skype: jv.ptec
Founder and Lead Developer



Alex Zatvorskiy

unread,
Oct 12, 2012, 7:42:13 AM10/12/12
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Works fine! Thanks )
Reply all
Reply to author
Forward
0 new messages