Is it possible to pass a pattern as an argument to a macro?

Hi there!

I’m wondering if it’s possible to pass a pattern to a macro (I assume I need a macro for this, which may be wrong!)

Basically, I want to write a function that would be called like:

iex> remove_newline_and_match_parts("a b c d e\n", ["a", _, _, _, _])
{:ok, ["a", "b", "c", "d", "e"]}

I’m attempting to implement this by writing a separate macro split_spaces_and_match(), and that’s where I’m having trouble, because I’m able to implement that sub-macro as:

defmacro split_spaces_and_match(data, pattern) do
    quote do
      result = String.split(unquote(data), " ")

      case result do
        unquote(pattern) = ret -> {:ok, ret}
        _ -> {:error, :pattern_match_failed}
      end
    end
  end

But then when I try to call that macro from remove_newline_and_match_parts(), things are being quoted differently and I’m very confused.

Thanks for any help anyone can offer!

Usually you’ll want to use functions for sub-parts of a complex macro, to avoid extra levels of quoting - try changing defmacro split_spaces_and_match to def split_spaces_and_match.

Won’t that prohibit me from passing it a pattern?

I’m confused, I took the code that you write and it literally worked as is:

defmodule Foo do
  defmacro remove_newline_and_match_parts(data, pattern) do
    quote do
      result = String.split(unquote(data), " ")

      case result do
        unquote(pattern) = ret -> {:ok, ret}
        _ -> {:error, :pattern_match_failed}
      end
    end
  end
end

import Foo

remove_newline_and_match_parts("a b c d e\n", ["a", _, _, _, _])
{:ok, ["a", "b", "c", "d", "e\n"]}

If you want to remove the new line you should also String.trim in addition to the String.split and you’re done.

All I had to do was rename the macro to be what you call. What issue are you running into?

Sorry, should clarify: I want to return {:error, :no_newline} if there’s a missing newline and {:error, :pattern_match_failed} if the match fails after removing the newline, so I’m doing something beyond String.trim(). I’d also like to be able to call just split_spaces_and_match() on its own, so I thought both need to be macros in order to accept a pattern.

Oh I see you’d do something like:

defmacro remove_newline_and_match_parts(string, pattern) do
  quote do
    with :ok <- unquote(__MODULE__).check_trailing_newline?(unquote(string)) do
      split_spaces_and_match(unquote(string), unquote(pattern))
    end
  end
end

def check_trailing_newline?(string) do
  # check that the string has a trailing new line.
end
1 Like

Ah ok great thanks so much!

1 Like

You only need one layer of defmacro to do that. For instance:

# the definition
defmacro remove_newline_and_match_parts(data, pattern) do
   # in here, "pattern" is the AST of the pattern
   nope(pattern)
   yep(pattern)
end

defmacro nope(pattern) do
  # pattern is the AST of accessing the local variable named "pattern"
  # this is likely NOT what you want!
end

def yep(pattern) do
  # pattern is still the AST of the pattern passed to remove_newline_and_match_parts
end
...

# the callsite

remove_newline_and_match_parts("a b c d e\n", ["a", _, _, _, _])

A frequent idiom for dealing with things like this - the defmacros for both remove_newline_and_match_parts and split_spaces_and_match juggle their arguments a little and then call the same private function to turn those arguments into output AST.

3 Likes

Awesome, thank you for the explanation - macros have a habit of overflowing the stack in my brain.