Trying to create a 'simple' macro, failing miserably

So, with macros, the more I read, the more confused I feel…

I wanted to try creating a macro that would let me write something like:

f = wire [:x, :y], do: x + y

where f would be a function like:

fn %{x: x, y: y} -> x + y end

My attempts at creating this have been pretty dismal though :frowning:
I’ve been looking at Plug and ExActor… but falling flat.
Any hints would be truly appreciated!

Thanks,
Scott S.

What would wire do?

It would be a macro that takes some list of atoms like [:x, :y] and a function body using variables
named after those atoms like x + y and returns an anonymous function that accepts a map with
those atom names as the keys and uses the function body provided.

Invoked it would behave like:

f = wire [:x, :y], do: x + y
8 = f.(%{x: 3, y: 5})

Does that make sense? Is it even possible?

I’m pretty sure it is, but I think learning would work out much better if you show us what you already tried and we try to explain why those don’t work and how to get them working. If we just gave you a macro that worked you wouldn’t learn much.

But I think you need var! due to hygiene.

3 Likes

Ok, I’ve tried so many variants :slight_smile:
(kind of embarrassed to even post my absolute confusion…)
Here’s one attempt with var!

I couldn’t find any Macro examples that returned anonymous functions – perhaps that’s a problem as well?

 defmodule Feed do

      defmacro wire(keys, body) do

        quote do
          keys = unquote(keys)
          map_arg = Enum.map(keys, fn k -> {k, var!(k)} end) |> Map.new()
          func_body = unquote(body[:do])
          fn (map_arg) -> func_body end
        end

      end

 end


    

Ideally, that would return a function that I could call:

f = Feed.wire [:a,:b], do: a + b
f.(%{a: 4, b: 7})
# returns 11

Thanks!

It wasn’t that bad I’d say. It’s just that working with AST is a bit different than coding with variables holding real values. It’s more a way to dynamically create other code so you actually need to create what the code would look like in AST form. Like an atom is not automatically a variable or making a map of compile time data is not necessarily the same as the AST form of a map.

defmacro wire(keys, do: body) do
  keys_to_variables =
    Enum.map(keys, fn key ->
      variable = quote do: var!(unquote(Macro.var(key, __MODULE__)))
      {key, variable}
    end)

  maparg = {:%{}, [], keys_to_variables}

  quote do
    fn unquote(maparg) -> unquote(body) end
  end
end

I’d suggest you to just go an do lot’s of quote do: … test to see how code looks in AST form, like:

iex(4)> quote do: %{a: var!(a), b: var!(b)}
{:%{}, [],
 [
   a: {:var!, [context: Elixir, import: Kernel], [{:a, [], Elixir}]},
   b: {:var!, [context: Elixir, import: Kernel], [{:b, [], Elixir}]}
 ]}

This is where you can see that the map is just a tuple with a keyword list as last element.

4 Likes

Nice thanks so much!!!

I will experiment with this quite a bit I think! :slight_smile:

1 Like