Core Erlang by Example
This blog post is the first about the Core Erlang format. In this blog post, we introduce the Core Erlang format through examples that compare Erlang code to the corresponding Core Erlang code.
I used the following command to translate my example module to Core Erlang code:
$ erlc +time +to_core core_example.erl
Compiling "core_example"
parse_module : 0.000 s 10.8 kB
transform_module : 0.000 s 10.8 kB
lint_module : 0.003 s 10.8 kB
expand_records : 0.000 s 10.8 kB
core : 0.000 s 89.9 kB
sys_core_fold : 0.000 s 58.6 kB
core_transforms : 0.000 s 58.6 kB
listing : 0.002 s 58.6 kB
The previous blog post
explored the passes from parse_module to expand_records. The
core passes translates from the abstract code to Core Erlang. We
will talk more about the Core Erlang passes in future blog posts.
I have slightly edited the examples to make them somewhat easier to read. There will be an unedited example at the very end of this blog post.
There’s a lot to cover, so let’s get started!
The simplest function #
Let start with the simplest possible function, a function with no arguments returning an atom:
simplest() -> 'ok'.
In Core Erlang, that will be:
'simplest'/0 =
fun () ->
'ok'
From that example, we can work out the following principles:
-
Atoms are always quoted.
-
Naming of the function has been separated from implementation of the function.
-
The body of a
funis not followed by anendas in Erlang.
Slightly less simple #
Here is as slightly more complicated function:
id(I) -> I.
In Core Erlang:
'id'/1 =
fun (_@c0) ->
_@c0
Note: All examples were compiled with OTP 20. The name of the generated variables will be different in the upcoming OTP 21.
Essentially, variables are named as in Erlang. In the translation to Core Erlang, the compiler generates new variable names for the arguments in a function head. The following code is also valid Core Erlang:
'id'/1 =
fun (I) ->
I
More than one clause #
Here is a function with more than one clause:
a(42) -> ok;
a(_) -> error.
In Core Erlang:
'a'/1 =
fun (_@c0) ->
case _@c0 of
<42> when 'true' ->
'ok'
<_@c2> when 'true' ->
'error'
end
-
A
funcan only have a single clause. -
Pattern matching must be done in a
case, not in thefunhead. -
Guards are mandatory for each clause in a
case. -
_is not a valid variable name in Core Erlang. Uninteresting values must be bound to a new variable. -
The
<and>around the patterns will be explained soon.
In Erlang, multiple function clauses can also be written with a
case like this:
b(N) ->
case N of
42 -> ok;
_ -> error
end.
The Core Erlang code will be essentially the same as the Core Erlang
code for a/1:
'b'/1 =
fun (_@c0) ->
case _@c0 of
<42> when 'true' ->
'ok'
<_@c3> when 'true' ->
'error'
end
Two clauses, three arguments #
Let’s try multiple arguments:
c(inc, Base, N) ->
Base+N;
c(_, Base, _) ->
Base.
In Core Erlang:
'c'/3 =
fun (_@c2,_@c1,_@c0) ->
case <_@c2,_@c1,_@c0> of
<'inc',Base,N> when 'true' ->
call 'erlang':'+'(Base, N)
<_@c6,Base,_@c7> when 'true' ->
Base
end
-
<and>denote a value list. The patterns in each clause in thecaseare always part of a value list. Thecaseexpression is a value list unless there is only one expression. -
Operators such as
+are not part of the Core Erlang language, so the compiler has translated the use of+to a call to the BIFerlang:'+'/2.
If #
Let’s see how if is implemented:
d(A, B) ->
if
A > B ->
greater;
true ->
not_greater
end.
In Core Erlang:
'd'/2 =
fun (_@c1,_@c0) ->
case <> of
<> when call 'erlang':'>'(_@c1, _@c0) ->
'greater'
<> when 'true' ->
'not_greater'
end
- The
caseexpression and the patterns are each value lists with zero elements. All the action is in the guards.
Repeated variables #
In Erlang, a variable can be repeated in a clause or within a pattern to indicate that the values must be the same:
cmp(Same, Same) -> same;
cmp(_, _) -> different.
Core Erlang does not allow repeating a variable:
'cmp'/2 =
fun (_@c1,_@c0) ->
case <_@c1,_@c0> of
<Same,_@c4> when call 'erlang':'=:='(_@c4, Same) ->
'same'
<_@c5,_@c6> when 'true' ->
'different'
end
- Here the second occurrence of the variable
Samehas been renamed to a new variable named_@c4, and a guard has been added to compareSameand_@c4.
Exceptions #
This function will fail with a function_clause exception if it is called
with any other value than 42:
e(42) -> ok.
In Core Erlang:
'e'/1 =
fun (_@c0) ->
case _@c0 of
<42> when 'true' ->
'ok'
<_@c1> when 'true' ->
primop 'match_fail'({'function_clause',_@c1})
end
-
A
casein Core Erlang must not fall off at the end, that is, there must always be a clause that will match. -
In this example, the last clause with a variable pattern and a
trueguard is guaranteed to match. -
The body for the last clause calls a primop to generate a function clause exception. Primops are primitive operations provided by the Erlang implementation, but not specified in the Core Erlang language specification.
Here is a similar function excepts that is uses case and therefore
will generate a case_clause exception if called with any other
argument than 42:
f(N) ->
case N of
42 -> ok
end.
The Core Erlang code is similar to the code for e/1:
'f'/1 =
fun (_@c0) ->
case _@c0 of
<42> when 'true' ->
'ok'
<_@c1> when 'true' ->
primop 'match_fail'({'case_clause',_@c1})
end
- The only difference is the argument for the
match_failprimop.
Let’s rewrite this function one more time:
g(N) ->
42 = N,
ok.
In Core Erlang:
'g'/1 =
fun (_@c0) ->
case _@c0 of
<42> when 'true' ->
'ok'
<_@c1> when 'true' ->
primop 'match_fail'({'badmatch',_@c1})
end
- Again, the only difference is the argument for the
match_failprimop.
Binding variables using ‘let’ #
Here is a function that binds the variable I:
h(A) ->
I = id(A),
I + A.
In Core Erlang:
'h'/1 =
fun (_@c0) ->
let <I> =
apply 'id'/1(_@c0)
in
call 'erlang':'+'(I, _@c0)
-
applycalls a fun or local function. -
The return value of the
applyis bound to the variableI. -
The variable
Ican only be used in the code that follows theinkeyword. -
The variable name is in a value list. That is because
letcan bind several variables at once.
Binding more than one variable in a ‘let’ #
Erlang has essentially no scoping. When a variable has been bound,
it remains bound to the end of the function. For example, variables bound
in a case can be used after the case:
i(E) ->
case E of
a ->
X = 1,
Y = 10;
b ->
X = 23,
Y = 17
end,
{X,Y}.
In Core Erlang:
'i'/1 =
fun (_@c0) ->
let <_@c7,X,Y> =
case _@c0 of
<'a'> when 'true' ->
<10,1,10>
<'b'> when 'true' ->
<17,23,17>
<_@c5> when 'true' ->
primop 'match_fail'({'case_clause',_@c5})
end
in
{X,Y}
-
A
casein Core Erlang does not export any variables. All variables that are to be used after thecasemust be explicitly returned. -
In this example, the first two clauses of the
casereturn a value list with three values. The first value is the return value of the case, which in this case is ignored. The other two values are the values assigned to theXandYvariables, respectively. -
The values returned from the
caseis bound in thelet. The ignored return value is bound to a new variable (_@c7), which is never used. The exported values are bound to theXandYvariables.
The unedited Core Erlang code #
So far all Core Erlang examples have been edited to make the points I am trying to make stand out clearer. Let’s have a look at the unedited version of a previous example:
'e'/1 =
%% Line 33
fun (_@c0) ->
case _@c0 of
<42> when 'true' ->
'ok'
( <_@c1> when 'true' ->
( primop 'match_fail'
({'function_clause',_@c1})
-| [{'function_name',{'e',1}}] )
-| ['compiler_generated'] )
end
-
The
-|associates an annotation with a Core Erlang construct. The meaning of an annotation is not specified in the Core Erlang language specification. -
The
compiler_generatedannotation associated with the last clause is a hint added by the compiler that subsequent optimization passes should not generate a warning if the clause was found to never match and dropped. -
The comment “Line 33” at the beginning is actually an annotation that the pretty printer has turned into a comment to avoid rendering the pretty-printed code unreadable.
Conclusion #
Core Erlang is less complicated than Erlang, and is therefore more suited than the abstract format for code analyzing tools (such as Dialyzer) and optimizers.
To learn more about Core Erlang #
All details can be found in Core Erlang 1.0.3 language specification.