DRY: what's the Erlang way of dealing with this scenario?

Hi all,

I have many modules that looks the same:

-module(m_one).
-behaviour(some_behaviour).
-export([some_f/2]).

some_f(X, Y) ->
    ...
    M = helpers:m_of(?MODULE)
    Z = erlang:apply(M, some_function, [])
    ...
-module(m_two).
-behaviour(some_behaviour).
-export([some_f/2]).

some_f(X, Y) ->
    ...
    M = helpers:m_of(?MODULE)
    Z = erlang:apply(M, some_function, [])
    ...

The only thing that changes in these modules is the M variable.

Is there a way to avoid repeating some_f in every single module?

Thank you.
Cheers!

1 Like

If you want some_f/2 to be exported from a module you must declare it there, and there is no way to avoid that. However, you can reduce repetition by moving the meaty bits to a helper module:

-module(helper).
-export([help/3]).

help(M0, X, Y) ->
    M = helpers:m_of(M0),
    Z = erlang:apply(M, some_function, []),
    %% ... et cetera
-module(m_one).
-behaviour(some_behaviour).
-export([some_f/2]).

some_f(X, Y) -> helper:help(?MODULE, X, Y).
-module(m_two).
-behaviour(some_behaviour).
-export([some_f/2]).

some_f(X, Y) -> helper:help(?MODULE, X, Y).
4 Likes

I have in my notes (and this may have changed over the many years) that local function calls are fastest, where external function calls are 3x slower, and erlang:apply/3 being 6x slower… If this is still true, then this should be considered when deciding if DRY is worth it or not.

I got this from an Erlang meetup, (19 September 2017, @ Klarna Stockholm)

Erlang efficiency guide
https://www.erlang.org/doc/efficiency_guide/functions
section 7.2 function calls

This is a rough hierarchy of the performance of the different types of function calls:

  • Calls to local or external functions (foo(), m:foo()) are the fastest calls.
  • Calling or applying a fun (Fun(), apply(Fun, )) is just a little slower than external calls.
  • Applying an exported function (Mod:Name(), apply(Mod, Name, )) where the number of arguments is known at compile time is next.
  • Applying an exported function (apply(Mod, Name, Args)) where the number of arguments is not known at compile time is the least efficient.

Most important of all is section 12.1, "do not guess about performance
https://www.erlang.org/doc/efficiency_guide/profiling#do-not-guess-about-performance—profile

Exact numbers are subject to variation from release to release (and of course the dizzying array of CPUs and their frequency adjustment policies) so there really is no substitute for measuring your own code on your own platform(s) on your own workload(s).

Being curious about what the current numbers could be, I ran a benchmark using erlperf:

% PATH=$ERL_TOP/bin:$PATH erlperf 'call_bm:apply(call_bm).' 'call_bm:external(call_bm).' 'call_bm:local(call_bm).'
Code                               ||        QPS       Time   Rel
call_bm:external(call_bm).          1   34762 Ki      28 ns  100%
call_bm:local(call_bm).             1   34710 Ki      28 ns  100%
call_bm:apply(call_bm).             1    7733 Ki     129 ns   22%

It seems that nowadays with the JIT, an external call is as fast as a local call, and an apply is between 4 and 5 times slower.

This benchmark was run on my M1 MacBook Pro. I got similar relative differences on my Intel iMac.

Here is the module I used for benchmarking:

-module(call_bm).
-export([apply/1,external/1,local/1,f/0]).

apply(M) ->
    M:f(),
    M:f(),
    M:f(),
    M:f(),
    M:f(),

    M:f(),
    M:f(),
    M:f(),
    M:f(),
    M:f(),

    M:f(),
    M:f(),
    M:f(),
    M:f(),
    M:f(),

    M:f(),
    M:f(),
    M:f(),
    M:f(),
    M:f().

external(_) ->
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),

    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),

    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),

    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f(),
    ?MODULE:f().

local(_) ->
    f(),
    f(),
    f(),
    f(),
    f(),

    f(),
    f(),
    f(),
    f(),
    f(),

    f(),
    f(),
    f(),
    f(),
    f(),

    f(),
    f(),
    f(),
    f(),
    f().

f() ->
    ok.
1 Like

Thanks for that.
Typical figures on my i5-6200U Ubuntu 22.04.03 laptop are

call_bm:local(call_bm). 1 8660 Ki 115 ns 100%
call_bm:external(call_bm). 1 7759 Ki 128 ns 90%
call_bm:apply(call_bm). 1 2788 Ki 358 ns 32%

using Erlang/OTP 24 [erts-12.2.1). Hmm. I’d upgraded the desktops.
Time to upgrade the laptops.

1 Like