Proposal: Add field puns/map shorthand to Elixir

proposal
on-topic-only-please

#86

My immediate thought on this is that it matches on nil for both keys. Similar to how if you leave the last body of a case statement empty, it will evaluate to nil.

defmodule Foo do
  def bar(baz) do
    case baz do
      true ->
        "Real value"
      _ ->
    end
  end
end

iex(1)> Foo.bar(true)
"Real value"
iex(2)> Foo.bar(false)
nil

Missing data looks like nil to me


#87

I’ve been looking at some libraries that implement this functionality using macros (not sigils, unlike short(er)_maps). I’ve discovered some interesting things about Elixir syntax.So, while this might be a little offtopic, I feel I must share this somewhat disturbing IEx session:

iex> %{a, b}
** (CompileError) iex:19: expected key-value pairs in a map, got: a
    (stdlib) lists.erl:1263: :lists.foldl/3

Move along, nothing extraordinary here…

iex> quote(do: %{a, b})
{:%{}, [], [{:a, [], Elixir}, {:b, [], Elixir}]}

Interesting…

iex> quote(do: %{a, :b})
{:%{}, [], [{:a, [], Elixir}, :b]}

Cool!

iex> quote(do: %{a, 1})
** (SyntaxError) iex:18: syntax error before: '}'

Waaaat?!

iex> quote do
...>   a = 1
...>   b = 2
...>   %{a, b}
...> end
{:__block__, [],
 [
   {:=, [], [{:a, [], Elixir}, 1]},
   {:=, [], [{:b, [], Elixir}, 2]},
   {:%{}, [], [{:a, [], Elixir}, {:b, [], Elixir}]}
 ]}

Hm… Nice!

iex> expr =
...>   quote do
...>     a = 1
...>     b = 2
...>     %{a, b}
...>   end
{:__block__, [],
 [
   {:=, [], [{:a, [], Elixir}, 1]},
   {:=, [], [{:b, [], Elixir}, 2]},
   {:%{}, [], [{:a, [], Elixir}, {:b, [], Elixir}]}
 ]}
iex> Code.eval_quoted(expr)
** (CompileError) nofile:1: expected key-value pairs in a map, got: a
    (stdlib) lists.erl:1263: :lists.foldl/3
    (elixir) lib/code.ex:590: Code.eval_quoted/3

Waaat? A compilation error while evaluating a quoted expression? (I know elixir compilation is actually the same as executing elixir code, but still…)

These examples show that the Elixir compiler has some very idiosyncratic ideas about what is valid Elixir. One wonders is most programmers using Elixir share those same ideas!


#88

Wow thanks @tmbb, I didn’t realize that %{bla} is represented by a valid AST. This opens up opportunities to add this language feature using macros!

In fact, I did a simple implementation and it works pretty well! You can enable it like this:

defmodule App do
  import Punning

  def example(%{field}), do: field

  def another_example(map), do
    %{field} = map
    field
  end

  def factory(bla, bla2), do: %{bla, bla2}
end

This works by extending def and defp, imputing the incomplete fields.


#89

You should look at the packages mentioned in the proposal. The latter two are macro based and already make use of the elixir parser parsing those.


#90

Ah I see Synex does a similar thing. The main difference is that using Punning there is no extra syntax except use-ing the lib.

Personally I think having to wrap maps in a (macro) function to enable punning defeats the purpose, and readability.


#91

I missed what you were doing on first read. Could you alternatively override the kernel special forms for % and %{}? If so, you would presumably be able to support it anywhere.


#92

Thanks, that would be even better! Not sure if that’s possible, but worth a try!

Edit: confirmed that special forms cannot be overwritten.


#93

If it’s not, maybe a proposal to allow it would be accepted.


#94

Yeah, I would have had no idea that syntax was “accepted” (for a given value of “accepted”) if it weren’t for those packages.

My goal here wasn’t to show that this is possible, it was to show that the Elixir compiler accepts some pretty weird stuff.


#95

You don’t need a language proposal for that. You just need this (with glorious warhammer-like ork speech):

defmodule MyModule do
  import BettaMapz
  with_betta_mapz do
    # Mapz are betta here!
  end
  # Mapz are normal again
end

EDIT: the difference is that this way it’s more verbose than is you were allowed to overwrite the special forms. If that were possible, a simple use BettaMapz would be enough.


#96

At some point in that discussion, I am pretty sure that I argue precisely that the colon sugar already introduces ambiguity and that’s exactly why we shouldn’t add more fuel to the fire. So the argument is not inconsistent, it was just trimmed. :slight_smile:

I know this feature is really desired but clear constraints have already been laid out. The JavaScript syntax has already been refused by different people and by different reasons. I don’t think persisting on it will help move the discussion forward.

Oh, thanks!


#97

@josevalim: do you have any ideas on how to implement this in userland without overwriting def and defp? Perhaps a way to apply a macro to all the code in the module, or allowing to overwrite Elixir.SpecialForms? This would allow to keep the Elixir language minimal, while also allowing for syntax experimentation in userland.

Another follow up idea: allow macro ast-transformers as plug-ins, to be configured in the mix.exs. With the code formatter it’s always an option to convert the project back to the original Elixir syntax.

This would also allow smooth transitions to breaking changes in Elixir, allow the old syntax using a plug-in.


#98

An important property of Elixir macros is that they are lexical and we have absolutely no plans to change it. Allowing project-wide or global mechanisms to change code is a recipe for disaster. :slight_smile:

We generally want the macro system to be as flexible as possible but the limitations on Kernel.SpecialForms are technical (we need a minimum set for the language to work) and the restriction on lexical macros are intentional.

One possible option would be figure out if we can make % no longer be a special form but a regular macro in Kernel. Someone would have to try it.


#99

Haha you are probably right :slight_smile:. Provides a lot of flexibility, but might also have damaging effects on the ecosystem.

Interesting option indeed. I might dive in this when I have the time!


#100

Right, quote gives us access to the syntax but when we execute we are talking about semantics. Generally the syntax is as general as possible and the semantics restricts it.

You did find a bug though. We are not supposed to allow %{a, :a} nor %{a, 1} at the moment. The bug appeared because we do allow %:atom{}, as a struct can be any module, and we ended-up using the same structure “struct expr” for the “map expr”. We should decouple those too.


#101

Is there a way for a macro to receive variable args? I believe that would be one of the constraints. If I do:

%{x, y, z: 1}

It would need to be possible to get x, y, z: 1 as a single AST, rather than individual items. Otherwise, you’d have to define a macro with arity for every number of elements that could be defined in a map.


#102

It isn’t But even if that was possible, I am 100% sure we need to implement maps as special forms otherwise it would be really, really, really hard to bootstrap the language. Structs are likely doable, since they are syntax sugar for maps anyway.


#103

You should indeed! I almost started a field punning library based on this bug.


#104

Not that I think that it is necessarily a good idea, but couldn’t you have a low level map which can’t be overwritten(say,{:map, [], args}`) and a user-facing map which you ultimately desugar to the lowe level map? I have no idea how this interferes with bootstrapping, though.


#105

Imagine we introduce __map__. Then it means all of Elixir code that is required for bootstrapping such as Kernel, Protocol, Enum, etc cannot use %{} and will have to use __map__ instead. That’s a no-no for me. :slight_smile: