RFC: Elixir Module and Struct Interoperability for Erlang
This is a RFC in the shape of a project that you can actually use, I'm interested in your feedback, find me as marianoguerra in the erlang and elixir slacks and as @warianoguerra on twitter.
The project on github: https://github.com/marianoguerra/exat and on hex.pm: https://hex.pm/packages/exat
See the exat_example project for a simple usage example.
Here's the description of the project:
Write erlang friendly module names and get them translated into the right Elixir module names automatically.
The project is a parse transform but also an escript to easily debug if the transformation is being done correctly.
Erlang Friendly Elixir Module Names
A call like:
Will be translated automatically to:
At build time using a parse transform.
The trick is that the @ symbol is allowed in atoms if it's not the first character (thank node names for that).
We use the ex@ prefix to identify the modules that we must translate since no one[1] uses that prefix for modules in erlang.
Aliases for Long Elixir Module Names
Since Elixir module names tend to nest and be long, you can define aliases to use in your code and save some typing, for example the following alias declaration:
Will translate ex@Bar:foo() to ex@Foo_Bar_Baz:foo() which in turn will become 'Elixir.Foo.Bar.Baz:foo()
It will also translate the module name bare:foo() into ex@Foo_Long:foo() which in turn will become Elixir.Foo.Long:foo()
Creating Structs
The code:
Becomes:
The code:
Becomes:
Which in Elixir would be:
Aliases in Structs
The following alias declaration:
Will expand this:
Into this:
Pattern Matching Structs
Function calls are not allowed in pattern match positions, for example on function/case/etc clauses or the left side of a =, for that there's a different syntax:
get_name({ex@struct_alias, #{name := Name}}) -> Name; get_name({ex@struct_alias, #{}}) -> {error, no_name}.
Becomes:
get_name(#{'__struct__' := 'Elixir.Learn.User', name := Name}) -> Name; get_name(#{'__struct__' := 'Elixir.Learn.User'}) -> {error, no_name}.
And:
Becomes:
#{'__struct__' := 'Elixir.Learn.User', name := _} = 'Elixir.Learn.User':'__struct__'(#{name => "bob", age => 42})
This is because that pattern will match maps that also have other keys.
Note on Static Compilation of Literal Structs
On Elixir if you pass the fields to the struct it will be compiled to a map in place since the compiler knows all the fields and their defaults at compile time, for now exat uses the slower version that merges the defaults against the provided fields using 'Elixir.Enum':reduce in the future it will try to get the defaults at compile time if the struct being compiled already has a beam file (that is, it was compiled before the current file).
Use
Add it to your rebar.config as a dep and as a parse transform:
Build
To build the escript:
$ rebar3 escriptize
Run
You can run it as an escript:
$ _build/default/bin/exat pp [erl|ast] path/to/module.erl
For example in the exat repo:
$ _build/default/bin/exat pp erl resources/example1.erl $ _build/default/bin/exat pp ast resources/example1.erl
Syntax Bikesheding
The syntax I chose balances the need to not produce compiler/linter errors or warnings with the objective of avoiding accidentally translating something that shouldn't be translated.
Please let me know what you think!
[1] Famous last words