Better way to write a macro to include a quoted expression in a string?

I want to be able to call my macro like this:

io_debug_inspect conn # TODO: Remove eventually.

and I want the expansion to end up like this:

IO.puts("`conn`: #{inspect(conn, limit: :infinity, pretty: true)}")

Here’s the macro I came up with:

  defmacro io_debug_inspect(t) do
    t_name = Macro.to_string(t)

    quote do
      IO.puts "`#{unquote(t_name)}`: #{inspect(unquote(t), limit: :infinity, pretty: true)}"
    end
  end

The macro tho expands to this:

IO.puts("`#{"conn"}`: #{inspect(conn, limit: :infinity, pretty: true)}")

Am I missing something? Or is the above good-enough? Or is it the closest I can get to what I originally wanted?

1 Like

You could construct the strings prefix outside of the quote and use <>/2 in the quote to concatenate prefix and inspection result.

1 Like

Like this?:

  defmacro io_debug_inspect(t) do
    prefix = "`#{Macro.to_string(t)}`: "

    quote do
      IO.puts prefix <> "#{inspect(unquote(t), limit: :infinity, pretty: true)}"
    end
  end

That produces expanded output like this, which is fine, but not obviously better:

IO.puts("`conn`: " <> "#{inspect(conn, limit: :infinity, pretty: true)}")

Your idea inspired this version:

  defmacro io_debug_inspect(t) do
    t_name = Macro.to_string(t)
    string = "\"`#{t_name}`: \#{inspect(#{t_name}, limit: :infinity, pretty: true)}\""

    quote do
      IO.puts unquote(string)
    end
  end

But, or so I’m guessing, string interpolation doesn’t work like above as the output is like this:

"`conn`: #{inspect(conn, limit: :infinity, pretty: true)}"

This also didn’t work:

  defmacro io_debug_inspect(t) do
    t_name = Macro.to_string(t)
    string = "\"`#{t_name}`: \#{inspect(#{t_name}, limit: :infinity, pretty: true)}\""

    quote do
      IO.puts unquote( Code.eval_string(string) )
    end
  end

Trying it:

iex> io_debug_inspect conn # TODO: Remove eventually.                                                 
warning: variable "conn" does not exist and is being expanded to "conn()", please use parentheses to remove the ambiguity or change the variable name
  nofile:1

** (CompileError) nofile:1: undefined function conn/0
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) src/elixir_bitstring.erl:142: :elixir_bitstring.expand_expr/4
    (elixir) src/elixir_bitstring.erl:27: :elixir_bitstring.expand/8
    (elixir) src/elixir_bitstring.erl:20: :elixir_bitstring.expand/4
    (elixir) lib/code.ex:232: Code.eval_string/3
    ...

I feel like there’s a way to do exactly what I originally wanted, but my original solution seems good enough.

But I’m curious as to how to match what I originally wanted, if it’s possible.

Nope, like this:

  defmacro io_debug_inspect(t) do
    prefix = "`#{Macro.to_string(t)}`: "

    quote do
      IO.puts prefix <> inspect(unquote(t), limit: :infinity, pretty: true)
    end
  end

Kernel.inspect does already return a string, no need to produce code that does an additional set of calls into String.Chars protocol.

2 Likes

It seems like it would be sensible to not try too hard to match some specific code when writing macros. Maybe just producing correct, and reasonable, code is the better target to aim for.

1 Like

This reminds me of the suggestion to add a rust-like dbg macro into elixir. ^.^

Why not this though?

  defmacro io_debug_inspect(t) do
    label = "`#{Macro.to_string(t)}`"

    quote do
      IO.inspect(unquote(t), limit: :infinity, pretty: true, label: unquote(label))
    end
  end
1 Like

I know that you’re looking for a macro as the solution for this, but an alternative to accomplish what appears to be your goal (and would work on any elixir project!) is to use an editor snippet. For example I have my editor setup so that when I type: lin<tab>conn it will add a line IO.inspect(conn, label: "conn") without me having to type conn twice. And of course it would be easy to add limit: :infinity, pretty: true to the snippet.

2 Likes

Yours is nicer – thanks!

Effectively, they seem to be very similar. After testing with a little map, the only real difference is that yours returns the value of t; the output seems to be exactly the same.

1 Like

That’s cheating! :wink:

I use Vim (really a GUI wrapper around Neovim), so I’m very much aware that I could use, e.g. a Vim macro, to generate this too.

But I share an Elixir project with someone that doesn’t use Vim! (I know – I’m still shocked by that discovery!) So, I figured a macro would be a little nicer for that person to use themselves; or any other future collaborators as well.

Obviously, macros are a bit of a trap – hence the general advice to avoid them where and when possible. Code generation is likewise strong evidence that either your programming language or tools are deficient in some significant way. But – sometimes – you need exactly that kind of booby-trapped implement to get something done.

This was all (mostly) an excuse to play around with Elixir macros. It is useful tho – I’ve been using it pretty regularly to debug integration tests for big, opaque blocks/trees of ‘legacy’ code.

2 Likes