PONY λ M2 Modula-2

Elm.CodeCompared.To/Erlang

An interactive executable cheatsheet comparing Elm and Erlang

Elm 0.19.1 Erlang/OTP 26
Basics & Output
Hello, World
Elm renders a pure main value. An Erlang shell expression just runs — io:format prints, ~n is the newline directive, and the period ends the expression.
module Main exposing (main) import Html exposing (text) main = text "Hello, World!"
io:format("Hello, World!~n").
You are looking at the language whose virtual machine made "functional and immutable" an industrial reality two decades before Elm. The surface could hardly look more different — module:function calls, tilde directives, terminating periods — but the substance underneath will keep feeling familiar.
Printing any value
Elm inspects with Debug.toString; Erlang's ~p directive pretty-prints any term, with the arguments passed as a list.
module Main exposing (main) import Html exposing (text) main = text (Debug.toString [ 1, 2, 3 ])
io:format("~p~n", [[1, 2, 3]]), io:format("~p and ~s~n", [42, "text"]).
The second argument to io:format/2 is always a list of values — hence the double brackets around the list being printed. ~p shows any term, ~s renders a string; unlike F#'s or OCaml's type-checked format strings, a mismatch here fails at runtime, because nothing in Erlang is checked before runtime.
Commas, semicolons, periods
Erlang punctuates like prose: commas separate expressions, semicolons separate clauses, and a period ends the whole thing.
module Main exposing (main) import Html exposing (text) greeting : String greeting = "Hello" main = text (greeting ++ ", punctuation!")
Greeting = "Hello", io:format("~s, punctuation!~n", [Greeting]), io:format("done~n").
The prose analogy is the standard mnemonic: , means "and then," ; means "or else" (between function or case clauses), and . means "the end." Elm needs none of this because layout is its punctuation. Getting a comma where a period belongs is every Erlang beginner's first dozen errors — budget for it.
Variables & Single Assignment
Stricter than Elm about names
Erlang variables start with a capital letter and are bound exactly once — = is a match assertion, and there is no rebinding and no shadowing. Elm finally has company.
module Main exposing (main) import Html exposing (text) answer : Int answer = 42 main = text (String.fromInt answer)
Answer = 42, Answer = 42, io:format("~p~n", [Answer]).
The second line is not a re-assignment — it is a match that succeeds because Answer already holds 42; Answer = 43 would crash with badmatch. Even OCaml lets you shadow and Elixir lets you rebind; Erlang and Elm stand alone in refusing both. An Elm developer's naming discipline is exactly the right preparation.
Matching is destructuring
module Main exposing (main) import Html exposing (text) main = let ( width, height ) = ( 3, 4 ) in text (String.fromInt (width * height))
{Width, Height} = {3, 4}, [First | Rest] = [1, 2, 3], io:format("~p ~p ~p~n", [Width * Height, First, Rest]).
Any pattern Elm can use in a case, Erlang uses on the left of =, anywhere — tuples, list cons, nested shapes. A failed match crashes the process, which is Erlang's runtime substitute for the exhaustiveness Elm proves at compile time; the crash-and-supervise machinery later in this page is what makes that acceptable.
Numbers & Strings
Division: / and div
module Main exposing (main) import Html exposing (text) main = text (String.fromInt (7 // 2) ++ " and " ++ String.fromFloat (7 / 2))
io:format("~p~n", [7 / 2]), io:format("~p~n", [7 div 2]), io:format("~p~n", [7 rem 2]).
Erlang's / always produces a float — 7 / 2 is 3.5, matching Elm's / — while div and rem are the integer pair, spelled as keywords rather than Elm's // and modBy.
Strings are lists (or binaries)
The strangest data-model fact on this page: a double-quoted Erlang "string" is literally a list of character codes. Modern Erlang prefers binaries — <<"…">> — for real text.
module Main exposing (main) import Html exposing (text) greeting : String greeting = "hello" main = text (greeting ++ " has " ++ String.fromInt (String.length greeting) ++ " characters")
Charlist = "hello", Binary = <<"hello">>, io:format("~s and ~s~n", [Charlist, Binary]), io:format("~p~n", [Charlist =:= [104, 101, 108, 108, 111]]), io:format("~p~n", [length(Charlist)]).
That final true is not a trick: "hello" IS [104, 101, 108, 108, 111], so list functions work on it and ~p may print a list of small integers as text. Binaries are the compact, modern representation (Elixir built its String type on them). Elm's opaque, Unicode-aware String is decades of hindsight applied to exactly this.
Funs & Recursion
Anonymous funs
At the shell, functions are funs: fun(Arguments) -> Body end, called through the variable that holds them.
module Main exposing (main) import Html exposing (text) add : Int -> Int -> Int add first second = first + second main = text (String.fromInt (add 2 3))
Add = fun(First, Second) -> First + Second end, io:format("~p~n", [Add(2, 3)]).
A fun's arity is fixed — Add(2) is a badarity crash, not a partial application, so Elm's automatic currying has no counterpart here (Erlang's libraries pass funs and full argument lists instead). Note the capital: Add is a variable holding a fun, and the call syntax Add(2, 3) uses it directly.
Clauses with semicolons
Like Elixir after it, Erlang moves case into the function head: a fun can have several clauses, separated by semicolons, tried top to bottom.
module Main exposing (main) import Html exposing (text) describe : Int -> String describe count = case count of 0 -> "none" 1 -> "one" _ -> "many" main = text (describe 1)
Describe = fun (0) -> "none"; (1) -> "one"; (_) -> "many" end, io:format("~s~n", [Describe(1)]).
Each clause is a pattern plus a body, and the semicolon reads as "or else try this clause." This is where Elixir's def factorial(0), do: 1 style comes from — Erlang had function-head pattern matching first. A value no clause matches crashes with function_clause, the runtime cousin of Elm's exhaustiveness error.
Recursion with named funs
A fun that needs to call itself takes an internal name — visible only inside its own body.
module Main exposing (main) import Html exposing (text) factorial : Int -> Int factorial number = if number <= 1 then 1 else number * factorial (number - 1) main = text (String.fromInt (factorial 5))
Factorial = fun Fact(0) -> 1; Fact(Number) -> Number * Fact(Number - 1) end, io:format("~p~n", [Factorial(5)]).
The internal name Fact lets the clauses recurse without reference to the outer Factorial binding. In a compiled module (see the last section) ordinary named functions recurse directly; the named-fun form is the shell's and this page's idiom. Recursion is, as in Elm, the only loop Erlang has.
There is no pipe
Elm's beloved |> does not exist in Erlang. The idiomatic substitute is naming each intermediate step — and single assignment makes those names trustworthy.
module Main exposing (main) import Html exposing (text) main = [ 1, 2, 3, 4, 5, 6 ] |> List.filter (\number -> modBy 2 number == 0) |> List.map (\number -> number * number) |> List.sum |> String.fromInt |> text
Numbers = [1, 2, 3, 4, 5, 6], Evens = lists:filter(fun(Number) -> Number rem 2 =:= 0 end, Numbers), Squares = lists:map(fun(Number) -> Number * Number end, Evens), Total = lists:sum(Squares), io:format("~p~n", [Total]).
Each named stage is immutable and bound exactly once, so the sequence reads like a pipeline with labeled checkpoints. The absence stung enough that Elixir's |> — running on this very VM — became its most celebrated feature. Note lists:filter takes the fun first and the list last, Elm's argument order rather than Elixir's.
Lists, Tuples & Maps
Lists & cons
module Main exposing (main) import Html exposing (text) main = text (Debug.toString (1 :: [ 2, 3 ]) ++ " " ++ Debug.toString ([ 1, 2 ] ++ [ 3, 4 ]))
Numbers = [2, 3], Longer = [1 | Numbers], io:format("~p~n", [Longer]), io:format("~p~n", [[1, 2] ++ [3, 4]]).
The same immutable singly-linked list as Elm's, with [Head | Tail] playing :: in both construction and patterns, and ++ meaning list concatenation (as it happens, the operator Elm reuses for strings). Prepending is cheap; appending walks the list.
The lists module
Elm's List.* vocabulary lives in Erlang's lists module — with the function argument first and the list last, exactly Elm's order.
module Main exposing (main) import Html exposing (text) main = List.range 1 4 |> List.map (\number -> number * 10) |> Debug.toString |> text
Doubled = lists:map(fun(Number) -> Number * 10 end, lists:seq(1, 4)), io:format("~p~n", [Doubled]), io:format("~p~n", [lists:foldl(fun(Number, Total) -> Number + Total end, 0, [1, 2, 3, 4])]).
lists:map, lists:filter, lists:foldl, lists:seq — the names and even the argument order will feel like home (Elm's List.foldl argument order matches too). This module is where Elm's List API ultimately traces its ancestry.
List comprehensions
Erlang has the comprehension syntax Elm declined — generators and filters between […||…].
module Main exposing (main) import Html exposing (text) main = List.range 1 10 |> List.filter (\number -> modBy 2 number == 0) |> List.map (\number -> number * number) |> Debug.toString |> text
Squares = [Number * Number || Number <- lists:seq(1, 10), Number rem 2 =:= 0], io:format("~p~n", [Squares]).
The generator Number <- lists:seq(1, 10) and the filter Number rem 2 =:= 0 compress Elm's two pipeline stages into one expression — the same construct Elixir's for comprehension wraps in friendlier clothes. Erlang borrowed it from the same well Haskell drew from.
Tuples
module Main exposing (main) import Html exposing (text) point : ( Int, String ) point = ( 1, "one" ) main = text (String.fromInt (Tuple.first point) ++ " is " ++ Tuple.second point)
Point = {1, "one"}, {Number, Name} = Point, io:format("~p is ~s~n", [Number, Name]), io:format("~p~n", [element(1, Point)]).
Curly braces, any arity, destructured by matching — and note element/2 indexes from one, not zero. Tuples carry most of Erlang's structured data, a role Elm splits between tuples and records.
Maps
Erlang maps — #{key => value} — are first-class literals that pattern-match, standing in for both Elm's Dict and (by convention, with atom keys) its records.
module Main exposing (main) import Dict import Html exposing (text) main = let ages = Dict.fromList [ ( "Ada", 36 ), ( "Grace", 45 ) ] in text (Debug.toString (Dict.get "Ada" ages))
Ages = #{"Ada" => 36, "Grace" => 45}, io:format("~p~n", [maps:get("Ada", Ages)]), #{"Ada" := AdaAge} = Ages, io:format("~p~n", [AdaAge]).
The match pattern #{"Ada" := AdaAge} pulls a value out by key — := in patterns, => in construction. Elixir's %{…} maps are these, resyntaxed. Elm's Dict does the same job through a function API with Maybe-typed lookups.
Atoms & Tagged Tuples
Atoms: constructors without the type
Lowercase bare words — ok, error, north — are atoms: constants that exist without declaration, playing Elm's nullary constructors.
module Main exposing (main) import Html exposing (text) type Direction = North | South describe : Direction -> String describe direction = case direction of North -> "up the map" South -> "down the map" main = text (describe North)
Direction = north, Description = case Direction of north -> "up the map"; south -> "down the map" end, io:format("~s~n", [Description]).
The capitalization rule inverts Elm's: Erlang atoms are lowercase and variables are capitalized, so north is a constant and North would be a variable binding. No type Direction closes the set — pass sideways and the case crashes at runtime instead of failing to compile.
{ok, Value} is where Result came from
Elm's Result formalizes a convention Erlang has used since the 1980s: return {ok, Value} or {error, Reason} and match on the tag.
module Main exposing (main) import Html exposing (text) safeDivide : Float -> Float -> Result String Float safeDivide numerator denominator = if denominator == 0 then Err "cannot divide by zero" else Ok (numerator / denominator) main = case safeDivide 10 2 of Ok quotient -> text (String.fromFloat quotient) Err reason -> text reason
SafeDivide = fun (_Numerator, 0) -> {error, "cannot divide by zero"}; (Numerator, Denominator) -> {ok, Numerator / Denominator} end, case SafeDivide(10, 2) of {ok, Quotient} -> io:format("~p~n", [Quotient]); {error, Reason} -> io:format("~s~n", [Reason]) end.
This is the ancestral form: F#'s Ok, Elm's Ok/Err, Rust's Result — all descend from this tagged-tuple habit. Erlang enforces none of it; the convention holds because forty years of libraries agree, and because an unhandled {error, Reason} match crashes loudly rather than propagating quietly.
case & Guards
case…of with when
Erlang's case … of is the construct Elm's case … of is named after — plus the when guards Elm left out.
module Main exposing (main) import Html exposing (text) describe : Int -> String describe number = if number == 0 then "zero" else if number < 0 then "negative" else if modBy 2 number == 0 then "positive even" else "positive odd" main = text (describe (-4))
Number = -4, Description = case Number of 0 -> "zero"; Value when Value < 0 -> "negative"; Value when Value rem 2 =:= 0 -> "positive even"; _ -> "positive odd" end, io:format("~s~n", [Description]).
Guards are restricted to a side-effect-free subset (comparisons, arithmetic, type tests) — the same discipline Elixir inherited. One vocabulary note: =:= is exact equality (Elm's ==), while Erlang's own == compares across numeric types, so 1 == 1.0 is true but 1 =:= 1.0 is false.
Absence Without Maybe
No Maybe — conventions instead
Erlang has no Maybe and no nil. Absence is handled by convention: a default argument, the undefined atom, or a tagged result — maps:find/2 returns exactly Elm's Maybe, untyped.
module Main exposing (main) import Dict import Html exposing (text) main = let ages = Dict.fromList [ ( "Ada", 36 ) ] in text (Debug.toString (Dict.get "Ada" ages) ++ " / " ++ Debug.toString (Dict.get "Grace" ages) )
Ages = #{"Ada" => 36}, io:format("~p~n", [maps:find("Ada", Ages)]), io:format("~p~n", [maps:find("Grace", Ages)]), io:format("~p~n", [maps:get("Grace", Ages, 0)]).
maps:find/2 returns {ok, 36} or the bare atom error — structurally Just 36 and Nothing, minus the type that forces handling. The three-argument maps:get/3 folds in a default like Maybe.withDefault. Nothing reminds you to handle the miss; the crash when you match {ok, Age} against error is the reminder.
Errors & Let It Crash
try/catch — and why you rarely see it
Erlang has exceptions and a try … catch to intercept them, matched by class and reason. Its culture, though, coined the opposite advice: let it crash.
module Main exposing (main) import Html exposing (text) -- Elm's only failure channel is a value. parsed : Result String Int parsed = String.toInt "not a number" |> Result.fromMaybe "parse failed" main = text (Debug.toString parsed)
Result = try error(boom) catch error:boom -> "caught boom" end, io:format("~s~n", [Result]).
The catch pattern error:boom matches the exception class and reason like any other pattern. But idiomatic Erlang wraps very little in try: expected failure travels as {error, Reason} values (Elm's instinct), and unexpected failure crashes the process, whose supervisor restarts it clean. Elm proves crashes impossible; Erlang makes them survivable — the two most principled answers to the same question.
Processes: TEA, Original Recipe
Messages are a primitive
Elm programs think in messages because Erlang programs thought in them first. spawn starts a process, ! sends, receive pattern-matches the mailbox.
module Main exposing (main) import Html exposing (text) type Msg = Greeting String update : Msg -> String -> String update message model = case message of Greeting name -> "Hello, " ++ name ++ "!" main = text (update (Greeting "Elm") "")
Parent = self(), spawn(fun() -> Parent ! {greeting, "Erlang"} end), receive {greeting, Name} -> io:format("Hello, ~s!~n", [Name]) end.
receive is a case … of over the process mailbox — tagged tuples as the Msg type, patterns as the branches. The Elm Architecture is this construct with the runtime holding the one mailbox for you; here there can be millions, one per process, each isolated with its own heap.
The Elm Architecture, original recipe
How immutable state "changes": a process whose receive loop recurses with the new value. Elm's update folded over messages is this loop with the runtime driving.
module Main exposing (main) import Html exposing (text) type Msg = Increment | Decrement update : Msg -> Int -> Int update message model = case message of Increment -> model + 1 Decrement -> model - 1 main = text (String.fromInt (List.foldl update 0 [ Increment, Increment, Decrement ]))
Loop = fun Loop(Count) -> receive increment -> Loop(Count + 1); decrement -> Loop(Count - 1); {get, Caller} -> Caller ! {count, Count}, Loop(Count) end end, Counter = spawn(fun() -> Loop(0) end), Counter ! increment, Counter ! increment, Counter ! decrement, Counter ! {get, self()}, receive {count, Value} -> io:format("~p~n", [Value]) end.
Set the two columns side by side: Elm's update : Msg -> Int -> Int IS the Loop fun's clauses, and folding over a message list IS the mailbox drained in order. Erlang invented this pattern for telephone switches in the 1980s; OTP's gen_server packages it, Redux popularized it for JavaScript, and Elm gave it a type system. You already know how to think here.
Modules & Files
What a real .erl file looks like
Everything above ran as shell expressions. Real Erlang code lives in compiled modules — attributes, an export list, and named multi-clause functions.
module Main exposing (main) import Html exposing (text) -- An Elm module: name, exposing list, definitions. circleArea : Float -> Float circleArea radius = 3.14159 * radius * radius main = text (String.fromFloat (circleArea 2))
-module(geometry). -export([circle_area/1, factorial/1]). circle_area(Radius) -> 3.14159 * Radius * Radius. factorial(0) -> 1; factorial(Number) -> Number * factorial(Number - 1).
Module attributes (-module, -export) cannot run at expression level, so this example is display-only: the eval runner (in the browser and in tests) executes shell expressions, not compilation units. Note what file-level code buys: named functions recurse directly, clauses still separate with semicolons, and the export list plays Elm's exposing. The circle_area/1 notation — name plus arity — is how Erlang identifies every function.