meyercm / shorter_maps

Elixir ~M sigil for map shorthand. `~M{id, name} ~> %{id: id, name: name}`
MIT License
231 stars 11 forks source link

Support for string interpolation #9

Open gridbox opened 7 years ago

gridbox commented 7 years ago

I have a use case in which I have a list of fields names that I would like to interpolate into the ~m and ~M sigils.

Current functionality

iex(9)> ~m{#{Enum.join(["hello","world"], ",")}} = %{"hello" => "hello", "world" => "world"}
** (FunctionClauseError) no function clause matching in Kernel.=~/2
    (elixir) lib/kernel.ex:1580: Kernel.=~({:::, [line: 9], [{{:., [line: 9], [Kernel, :to_string]}, [line: 9], [{{:., [line: 9], [{:__aliases__, [counter: 0, line: 9], [:Enum]}, :join]}, [line: 9], [["hello", "world"], ","]}]}, {:binary, [line: 9], nil}]}, ~r/\A\s*[a-zA-Z_]\w*\s*\|/)
             lib/shorter_maps.ex:106: ShorterMaps.get_old_map/1
             lib/shorter_maps.ex:78: ShorterMaps.do_sigil_m/3
             expanding macro: ShorterMaps.sigil_m/2
             iex:9: (file)
iex(9)> ~M{#{Enum.join(["hello","world"], ",")}} = %{hello: "hello", world: "world"}        
** (SyntaxError) iex:9: unexpected token: }

Desired functionality

iex(9)> ~m{#{Enum.join(["hello","world"], ",")}} = %{"hello" => "hello", "world" => "world"}
%{"hello" => "hello", "world" => "world"}
iex(9)> ~M{#{Enum.join(["hello","world"], ",")}} = %{hello: "hello", world: "world"}        
%{hello: "hello", world: "world"}
meyercm commented 7 years ago

So there are three problems in our way.

First, your desired syntax of ~m{ #{...} } will probably never work, as I was unable to make a good enough argument for nested braces in this Elixir issue. Currently, the sigil will be terminated with the first }, which will be found at the end of your interpolation (This is the actual error in your example, but it still wouldn't work, see below). The easy work around is to just use ~m( #{...} ). The harder solution is to succeed where I failed, and convince @josevalim that embedding braces in sigils is worthwhile.

Second: ShortMaps, the project we started with, didn't support interpolation, and when I copied the codebase, I never thought to add it. This is probably the easiest of our three hurdles.

Third, and most difficult to circumnavigate. Consider this simple module:

defmodule TestSigils do
  defmacro sigil_z(args, opts), do: Macro.escape(args)
  defmacro sigil_Z(args, opts), do: Macro.escape(args)
end
iex> import TestSigils
iex> ~Z(#{:a})
{:<<>>, [line: 2], ["\#{:a}"]}
iex> ~z(#{:a})
{:<<>>, [line: 3],
 [{:::, [line: 3],
   [{{:., [line: 3], [Kernel, :to_string]}, [line: 3], [:a]},
    {:binary, [line: 3], nil}]}]}

In case that isn't clear; ~Z is actually receiving different parameters from the compiler. This is a surprise to me, but careful re-reading of the Elixir Sigils documentation proves that is the intended behavior, even if it seems like something that should be left up to the sigil (as I thought it was). I'm not sure I see a great way around this problem while continuing to use Sigils... the best I have is to use the ~m(...)a construction, which I generally disdain.

In the destructure library, the author bypasses sigils, and writes a macro d() to accomplish the map expansions. I don't think I like the paren-omissions and the % inclusion that he uses, but we could add a m() and M() macro to ShorterMaps to accomplish the string interpolation goal, as well as gain back syntax highlighting.

Thoughts?