Erlang Notes

other BEAM languages
Login

other BEAM languages

  1. general annoyances
  2. LFE -> Erlang
  3. linting LFE with dialyzer
  4. Elixir -> Erlang
  5. 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:

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.