- general annoyances
- LFE -> Erlang
- linting LFE with dialyzer
- Elixir -> Erlang
- Using an Elixir library in an Erlang escript
general annoyances
Inter-language communication is through .beam files and, just like advanced languages having to communicate through the C ABI, features are lost in translation -- especially compiletime features, such as macros and type information. The different styles of the languages also bring them into conflict: Elixir's pipeline ordering isn't respected by Erlang, Elixir modules and LFE dashed-names require quoting when used by Erlang, tooling and build systems are language-specific. (And then there's truly weird stuff like Forth implementations where exported functions return a tuple with threaded code and can't be executed directly.)
LFE -> Erlang
(defmodule demolfe
(export (main 0) (fac 1)))
(defun main ()
(lfe_io:format "hello world~n" ()))
(defun fac
((1) 1)
((n) (* n (fac (- n 1)))))
LFE's rebar3 interaction has support for building projects, but seemingly not individual files. This can be compiled with the repl:
lfe> (c "demolfe") (#(module demolfe)) lfe> (demolfe:fac 40) 815915283247897734345611269596115894272000000000
And although fac/1 can be used directly, lfe_io of course is a library that's not available without more work:
$ erl -noshell -eval 'erlang:display(demolfe:fac(40)), init:stop().'
815915283247897734345611269596115894272000000000
$ erl -noshell -eval 'demolfe:main(), init:stop().'
Error! Failed to eval: demolfe:main(), init:stop().
Runtime terminating during boot ({undef,[{lfe_io,format,["hello world~n",[]],[]},{erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,900}]},{erl_eval,exprs,6,[{file,"erl_eval.erl"},{line,271}]},{init,start_it,1,[]},{init,start_em,1,[]},{init,do_boot,3,[]}]})
Crash dump is being written to: erl_crash.dump...done
Or interactively:
1> m(demolfe).
Module: demolfe
MD5: ...
Object file: /path/to/demolfe.beam
Compiler options: [nowarn_unused_vars]
Exports:
'LFE-EXPAND-EXPORTED-MACRO'/3
fac/1
main/0
module_info/0
module_info/1
ok
2> demolfe:fac(40).
815915283247897734345611269596115894272000000000
3> demolfe:main().
** exception error: undefined function lfe_io:format/2
How much trouble is getting that library? Just add the .beam files to the search path:
$ erl -noshell -pz $HOME/.cache/rebar3/plugins/lfe/ebin -eval 'erlang:display(demolfe:fac(40)), demolfe:main(), init:stop().' 815915283247897734345611269596115894272000000000 hello world
Idiomatic LFE, like Lisp, will have dashes in names and therefore the functions will require quoting to use from Erlang, as with the all-caps function from above.
linting LFE with dialyzer
Consider this LFE translation of discrep2.erl, from Learn You Some Erlang's chapter on dialyzer:
(defmodule e2lfe
(export (run 0)))
(defun run ()
(let ((tup (money 5 'you)))
(some-op (count tup) (account tup))))
(defun money (num name)
(tuple 'give num name))
(defun count
(((tuple 'give num _)) num))
(defun account
(((tuple 'give _ x)) x))
(defun some-op (a b)
(+ a b))
Dialyzer can't check this directly, but dialyzer can read .beam files instead. However, it doesn't like LFE .beam files by default:
$ dialyzer e2lfe.beam Checking whether the PLT .../.cache/erlang/.dialyzer_plt is up-to-date... yes Proceeding with analysis... dialyzer: Analysis failed with error: Could not scan the following file(s): Could not get Core Erlang code for: .../e2lfe.beam Last messages in the log cache: Reading files and computing callgraph...
This is due to the .beam file not having debug info. That's easily fixed:
lfe> (c "e2lfe" '(debug-info)) (#(module e2lfe))
And then dialyzer can handle it. The output's not quite as friendly as on the Erlang example, but it's still very usable:
$ dialyzer e2lfe.beam
Checking whether the PLT .../.dialyzer_plt is up-to-date... yes
Proceeding with analysis...
e2lfe.lfe:4: The call e2lfe:'some-op'
(5,
'you') will never return since it differs in the 2nd argument from the success typing arguments:
(number(),
number())
e2lfe.lfe:4: Function run/0 has no local return
e2lfe.lfe:15: The call erlang:'+'(-a-0-::5,-b-0-::'you') will never return since it differs in the 2nd argument from the success typing arguments:
(number(),
number())
e2lfe.lfe:15: Function 'some-op'/2 has no local return
done in 0m0.08s
done (warnings were emitted)
Elixir -> Erlang
defmodule DemoElixir do
def main do
IO.puts "Hello, world!"
end
def fac(0), do: 1
def fac(n) when n > 0 do
n * fac(n - 1)
end
end
This can be compiled at the CLI:
$ elixirc demoelixir.exs
Annoying module names aside, this is very similar to LFE:
4> m('Elixir.DemoElixir').
Module: 'Elixir.DemoElixir'
MD5: ...
Object file: /path/to/Elixir.DemoElixir.beam
Compiler options: [no_spawn_compiler_process,from_core,no_core_prepare,
no_auto_import]
Exports:
'__info__'/1
fac/1
main/0
module_info/0
module_info/1
ok
5> 'Elixir.DemoElixir':fac(40).
815915283247897734345611269596115894272000000000
6> ** exception error: undefined function 'Elixir.IO':puts/1
The names really are annoying:
- they break module name completion at eshell
- I have to use double-quotes in this next oneliner
But those libraries of course can still be put in the search path:
$ erl -noshell -pz $HOME/.asdf/installs/elixir/1.16.2-otp-26/lib/elixir/ebin -eval "'Elixir.DemoElixir':main(), init:stop()." Hello, world!
Using an Elixir library in an Erlang escript
For example, https://github.com/dbernheisel/date_time_parser.
using this library required this addition to rebar.config (the 'consolidate' hook does nothing):
{deps, [
{date_time_parser, "~> 1.2.0"}
]}.
{plugins, [rebar_mix]}.</code
With that, rebar3 shell, after quite a bit of compilation, does expose the library:
1> 'Elixir.DateTimeParser':parse(<<"Sunday, 24 Sep 2023">>).
{ok,#{calendar => 'Elixir.Calendar.ISO',month => 9,
'__struct__' => 'Elixir.Date',day => 24,year => 2023}}
2> {ok, #{day:=D, month:=M, year:=Y}} = 'Elixir.DateTimeParser':parse(<<"Sunday, 24 Sep 2023">>), {D,M,Y}.
{24,9,2023}
And although this bloats the _build directory to 40 MB, the escript is still only 2.8K?
It's so small because it doesn't include the Elixir library!
$ ./_build/default/bin/kal escript: exception error: undefined function 'Elixir.DateTimeParser':parse/1
The missing configuration is escript-specific:
{escript_incl_apps, [date_time_parser]}.
New escript size: 5.6 MB.