Es6_maps - EcmaScript6-style shorthand map literals

I’ve just released what I believe is a production-ready version of my library, es6_maps.

es6_maps hacks into the Elixir compiler to enable EcmaScript6-like shorthand map usage that you might know from JavaScript or Rust:

iex> foo = 1
iex> %{hello, bar} = %{foo, hello: "world", bar: 2}
%{foo: 1, hello: "world", bar: 2}
iex> hello
"world"

There’s already a popular library doing a similar thing, shorter_maps, that doesn’t need any module-replacing shenanigans in order to do its thing. The motivation for implementing es6_maps instead of using shorter_maps was that:

  1. I do firmly believe this is a good language feature and a very natural extension of map literals syntax;
  2. at the same time, being a language feature it should be simple - es6_maps works only with atom keys and has no extra features over the key expansion.

Point 1 is additionally reinforced by how easy it was to introduce to the compiler - I’m injecting just 9 lines of simple code, while parser and lexer already accept short-form maps without modifications.

The library also includes a Mix task for jumping all-in to shorthand map forms - as well as reverting all of the shorthand literals back into expanded ones in case you’d like to remove the dependency.

GitHub: GitHub - kzemek/es6_maps: ES6-like shorthand syntax for Elixir maps: `%{foo, bar} = map; IO.puts(foo)`
Hex: es6_maps | Hex
HexDocs: es6_maps v0.2.1 — Documentation

10 Likes

Is there a reason why you have created a custom mix task for formatting instead of writing a plugin for .formatter.exs file?

By the nature of this solution it’s tightly coupled to the internal Elixir implementation. The current version of es6_maps should work for Elixir 1.15, 1.16 and the upcoming 1.17 version, but may break in the future.

Could you please add some kind of verification to the compile task (with special form maybe?), so the code would not fail in said task and it would automatically call --revert and print a warning instead? Is it possible in this case?

Is there a reason why you have created a custom mix task for formatting instead of writing a plugin for .formatter.exs file?

A couple:

  • It’s very hard to pass-through formatting when implementing Mix.Tasks.Format behaviour. If your plugin says it handles [".ex", ".exs"] extensions, it will take over these extensions completely, and the original formatter won’t run anymore.
    In its initial incarnation the formatter only concerned itself with maps, leaving the rest of the formatting up to the default formatter. It’s true that the current version basically does the whole format though, calling Mix.Format just in case…
  • But more importantly, I included a formatting task as a way to jump all-in, but primarily as a guarantee for people that they’ll be able to jump out if needed. I did see that as a one-off, explicitly called task.

That said, with the current implementation it might make sense to both have the task and an optional formatter for people to use, sharing the same code.

Could you please add some kind of verification to the compile task (with special form maybe?), so the code would not fail in said task and it would automatically call --revert and print a warning instead? Is it possible in this case?

It is possible, but I don’t think I can sensibly guard / detect against incompatible changes in the compiler without knowing what the changes would be. Also note that es6_maps only modifies the compiler that runs in a separate pass, it doesn’t compile things by itself.

To be clear, if I could detect that the code would fail to compile due to the shorthand-form maps I wouldn’t reformat user’s on-disk files anyway – and, if I found a good way to modify AST and pass it to the compiler, I would have rewritten the whole library to use that mechanism instead and there wouldn’t be a need for a fallback in the first place.

1 Like

Will LSP server (e.g. elixir-ls) work with this library?

:star_struck: This is awesome!

Will LSP server (e.g. elixir-ls) work with this library?

I’m using ElixirLS, and while it doesn’t pick up e.g. hello in %{hello} = %{hello: 1} as a new variable for autocomplete, it also doesn’t throw up any warnings or errors to the user - because the syntax is correct and the code compiles. It does however complain about the line in its internal warning output.

Getting more support in an LS is an interesting point of improvement. I wonder if I could influence it from the “outside” similarly to how I do the compiler.

Is there a reason why you have created a custom mix task for formatting instead of writing a plugin for .formatter.exs file?

Following our discussion, I have just released v0.2.2 where the custom format task is replaced with a formatting plugin that can be additionally configured inline with comment pragmas.

1 Like

I looked at the implementation, and I must say that I’m impressed by the flexibility of Elixir and BEAM. It’s wicked, but it’s good to know that you can modify the behavior this deeply.

These shorthand map keys look extremely useful to me personally, I find that my map keys and variable names are often the same. Are there any prior discussions about making this shorthand notation part of “standard” compiler? Would be interesting to know why developers decided against it.

This discussion is at least as old as elixir is stable. This is from just a few month after 1.0 and it still mentions earlier proposals as well: https://groups.google.com/g/elixir-lang-core/c/NoUo2gqQR3I

3 Likes

I believe this is the most recent extended discussion complete with proposal and poll: https://elixirforum.com/t/proposal-add-field-puns-map-shorthand-to-elixir/15452

3 Likes

FWIW, I think this feature could likely get accepted into Elixir. The main problem IMO is that most people want bare words, but Jose wants there to be a visible distinction so it’s clear what the key type is. I think if someone proposed the ruby style syntax for this instead and was willing to do the work to implement it, it’d be accepted.

Basically, instead of:

iex> foo = 1
iex> %{hello, bar} = %{foo, hello: "world", bar: 2}

do:

iex> foo = 1
iex> %{hello:, bar:} = %{foo:, hello: "world", bar: 2}

I don’t see anywhere in your examples anything about the pin operator (^), map update syntax %{x | foo: 1}, structs %Foo{bar: 1}, or struct update syntax %Foo{baz | bar: 1}. If they’re not supported, I’d argue they should be added.

I assume this part isn’t directed at me?

For the record I’m personally opposed to this making it into the language and think it’s better it exists in libraries. I just wanted to share that thread as it has the most discussion and, in spite of my feelings, your proposal was an epic-good read and lesson in proposal writing. I still think back to it when I try and outline tech concepts in writing :joy:

1 Like

No, not meant to be directed at you. Thanks for the kind words.

I’d really like to have that feature.

Did you think about adding sth to convert between normal and es6-maps-codebases.
Would be easier to use in teams.

I’d personally be very happy with a solution like that built into the language.

Assuming you’re talking about es6_maps:

The pin operator support is something I’d like to see in the library in the future – the bigest impediment being that %{^x, y: 1} is not valid syntax and doesn’t make it to the AST stage, unlike %{x, y: 1}.

Map & struct updates are supported and there’s a couple examples in the README.

From my PoV the problem is, it’s really tricky to implement this as a library in a way that feels like a natural part of the language.

I was fine with requiring use Es6Maps in each module that wanted shorthands, but between @on_definition deliberately not calling macros, @before_compile actually happening after a compilation pass, and this post from @josevalim, there weren’t many options left aside from modifying the compiler in-flight to get this. It would be great to be able to create language extension PoCs that could make it into the standard implementation.

Already added, there’s an Es6Maps.Formatter included in the library. You add it as plugins: [Es6Maps.Formatter] to .formatters.exs, call mix format, and all of your maps will be converted.

Unless you mean something like “display shorthand maps in the editor, save normal maps to disk”? That sounds like an interesting feature for ElixirLS.

1 Like