Output & Running
Hello, World
io:format("Hello, World!~n"). object Main {
def main(args: Array[String]): Unit = {
println("Hello, World!")
}
} Erlang prints with
io:format/1 and the ~n directive for the newline. Every runnable Scala program needs a main method on an object; println appends the newline for you and needs no import, since it comes from the automatically-included Predef.Printing a value for inspection
Value = {ok, [1, 2, 3]},
io:format("~p~n", [Value]). object Main {
def main(args: Array[String]): Unit = {
val value = Some(List(1, 2, 3))
println(value)
}
} Erlang's
~p ("pretty print") directive renders any term for humans, including nested tuples and lists. Scala's println relies on every object's inherited toString, which case classes and standard collections already implement sensibly — the output shown, Some(List(1, 2, 3)), needs no extra formatting call.Dynamic vs. Static Types
Runtime checking vs. compile-time checking
% No type declaration anywhere — this only fails when it actually RUNS:
Add = fun(X, Y) -> X + Y end,
io:format("~p~n", [Add(2, 3)]). object Main {
def add(x: Int, y: Int): Int = x + y
def main(args: Array[String]): Unit = {
println(add(2, 3))
}
} Erlang has no compile-time type checking at all —
Add("two", 3) would compile fine and only fail when that exact line executes. Scala's def add(x: Int, y: Int): Int means add("two", 3) is rejected before the program ever runs, caught by the JVM's static type system rather than discovered in production.Local type inference feels familiar
Double = fun(X) -> X * 2 end,
io:format("~p~n", [Double(21)]). object Main {
def main(args: Array[String]): Unit = {
val result = 21 * 2
println(result)
}
} Scala infers the type of
result from context, so day-to-day local code reads almost as unannotated as Erlang — you rarely write val result: Int = ... explicitly. The difference only surfaces at function boundaries: Scala method parameters and return types are (with rare exceptions) always explicit, the one place static typing cannot infer its way past a genuine ambiguity.Optional analysis vs. mandatory compilation
% Erlang's Dialyzer can catch SOME type errors, but only if you
% run it separately and add -spec annotations — it is opt-in, not enforced:
% -spec add(integer(), integer()) -> integer().
Result = 2 + 3,
io:format("~p~n", [Result]). object Main {
def add(x: Int, y: Int): Int = x + y
def main(args: Array[String]): Unit = {
println(add(2, 3))
}
} Erlang's closest equivalent to Scala's type checker is Dialyzer, a separate static-analysis tool reading optional
-spec annotations that a project can ship without ever running. In Scala, scalac is not an optional linter — the compiler simply refuses to produce a running program if the types disagree.Pattern Matching
Function clauses become a `match` expression
Describe = fun
Describe(0) -> "zero";
Describe(N) when N > 0 -> "positive";
Describe(_) -> "negative"
end,
io:format("~s~n", [Describe(-5)]). object Main {
def describe(n: Int): String = n match {
case 0 => "zero"
case _ if n > 0 => "positive"
case _ => "negative"
}
def main(args: Array[String]): Unit = {
println(describe(-5))
}
} Erlang picks a function clause by matching the call against each one, top to bottom; Scala's
match expression does the identical thing inside a single function, one case per Erlang clause. The when N > 0 guard becomes an if n > 0 guard on the case arm — same idea, attached in a slightly different place.Destructuring a tuple
Result = {ok, 42},
{ok, Value} = Result,
io:format("~p~n", [Value]). object Main {
def main(args: Array[String]): Unit = {
val result = (true, 42)
val (_, value) = result
println(value)
}
} Both languages let you destructure a tuple directly in a binding. Erlang tuples are untyped and can hold anything of any arity; Scala tuples like
(Boolean, Int) are typed by position, so mismatched destructuring is caught at compile time rather than crashing on an unlucky input.Guards inside a match
Classify = fun(N) ->
case N of
X when X rem 2 =:= 0 -> even;
_ -> odd
end
end,
io:format("~p~n", [Classify(7)]). object Main {
def classify(n: Int): String = n match {
case x if x % 2 == 0 => "even"
case _ => "odd"
}
def main(args: Array[String]): Unit = {
println(classify(7))
}
} Erlang can guard a
case arm with when exactly the way it guards a function clause; Scala mirrors this with an if guard on a match case. Both read as "this shape, but only when this extra condition also holds."Tagged Tuples vs. Option
`{ok, X}` becomes `Some(x)`
Lookup = fun(Key, List) ->
case lists:keyfind(Key, 1, List) of
{Key, Value} -> {ok, Value};
false -> error
end
end,
io:format("~p~n", [Lookup(b, [{a, 1}, {b, 2}])]). object Main {
def main(args: Array[String]): Unit = {
val entries = Map("a" -> 1, "b" -> 2)
println(entries.get("b"))
}
} Erlang's convention of returning
{ok, Value} on success and a bare error on failure is exactly what Scala's built-in Option formalizes: Some(value) is {ok, Value}, and None is the failure case. Map.get already returns an Option, so this pattern is baked into the standard library rather than something you build by hand.Skipping the failure case is a compile error
% Erlang: nothing stops you from assuming success — this would crash
% with a badmatch if Lookup returns error instead of {ok, _}; caught
% here with try/catch so the crash is visible instead of halting the page:
try
{ok, Value} = case lists:keyfind(b, 1, [{a, 1}]) of
{b, V} -> {ok, V};
false -> error
end,
io:format("~p~n", [Value])
catch
error:{badmatch, _} -> io:format("crashed: badmatch~n")
end. object Main {
def main(args: Array[String]): Unit = {
val entries = Map("a" -> 1)
entries.get("b") match {
case Some(value) => println(value)
case None => println("not found")
}
}
} Nothing in Erlang forces you to handle the failure branch — skipping it just defers the crash to whenever a bad input finally reaches that assumption. Treating a Scala
Option[Int] as a bare Int is a type error the compiler catches immediately; matching both Some and None is the idiomatic way to satisfy it.Transforming inside `Option`
Transform = fun
({ok, Value}) -> {ok, Value * 2};
(error) -> error
end,
io:format("~p~n", [Transform({ok, 21})]). object Main {
def main(args: Array[String]): Unit = {
val value: Option[Int] = Some(21)
println(value.map(_ * 2))
}
} Erlang has no built-in "transform the value if present, otherwise leave the failure alone" helper — you write the two-clause pattern match by hand every time. Scala's
Option.map encapsulates exactly this: applying a function only inside Some, and leaving None untouched, with no explicit branching required.{error, Reason} vs. Either
`{error, Reason}` becomes `Left(reason)`
Divide = fun(_, 0) -> {error, divide_by_zero};
(X, Y) -> {ok, X div Y}
end,
io:format("~p~n", [Divide(10, 0)]). object Main {
def divide(x: Int, y: Int): Either[String, Int] =
if (y == 0) Left("divide by zero") else Right(x / y)
def main(args: Array[String]): Unit = {
println(divide(10, 0))
}
} Where
Option only says "did it work," Scala's Either[L, R] carries a reason on failure — Left(reason) corresponds directly to Erlang's {error, Reason}, and Right(value) corresponds to {ok, Value}. By convention Right is the success case (a mnemonic: "right" also means "correct"), the same way ok tuples are always the success case in idiomatic Erlang.Exceptions exist too, but are typically local
% Erlang: a process-wide crash, caught here so it doesn't
% halt the whole example:
try 10 div 0 of
Result -> io:format("~p~n", [Result])
catch
error:badarith -> io:format("crashed: badarith~n")
end. object Main {
def main(args: Array[String]): Unit = {
try {
println(10 / 0)
} catch {
case _: ArithmeticException => println("crashed: divide by zero")
}
}
} Scala does have JVM-style exceptions and
try/catch, syntactically close to Erlang's own try/catch. The philosophical difference is emphasis: idiomatic Scala tends to reserve exceptions for genuinely exceptional conditions and models expected failures with Option/Either instead, closer to how idiomatic Erlang reserves an actual crash for "let a supervisor restart me" rather than routine error signaling.Records vs. Case Classes
Records vs. a genuine case class
% Erlang records are compile-time sugar for tagged tuples — no runtime
% checking beyond field-name shape:
Point = {point, 3, 4},
{point, X, Y} = Point,
io:format("~p and ~p~n", [X, Y]). case class Point(x: Int, y: Int)
object Main {
def main(args: Array[String]): Unit = {
val point = Point(3, 4)
println(s"${point.x} and ${point.y}")
}
} Erlang's
-record(point, {x, y}). desugars to a tagged tuple at compile time — a naming convenience with no runtime type distinct from any other tuple of the same shape. Scala's case class Point(x: Int, y: Int) introduces a genuinely distinct type, plus free equality, a sensible toString, and pattern-matching support, none of which Erlang's records provide automatically.Case classes destructure in patterns
Point = {point, 3, 4},
{point, X, Y} = Point,
io:format("Sum: ~p~n", [X + Y]). case class Point(x: Int, y: Int)
object Main {
def main(args: Array[String]): Unit = {
val point = Point(3, 4)
point match {
case Point(x, y) => println(s"Sum: ${x + y}")
}
}
} Case classes come with pattern-matching support built in for free —
case Point(x, y) => destructures exactly the way matching an Erlang tagged tuple {point, X, Y} does, down to naming the fields positionally. This is the feature that gives case classes their name: they exist specifically to be pattern-matched.Tagged Tuples vs. Sealed Traits
Modeling "one of several shapes"
Shapes = [{circle, 5}, {rectangle, 3, 4}],
Areas = lists:map(fun
({circle, Radius}) -> 3.14159 * Radius * Radius;
({rectangle, Width, Height}) -> Width * Height
end, Shapes),
io:format("~p~n", [Areas]). sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
object Main {
def area(shape: Shape): Double = shape match {
case Circle(radius) => 3.14159 * radius * radius
case Rectangle(width, height) => width * height
}
def main(args: Array[String]): Unit = {
val shapes = List(Circle(5), Rectangle(3, 4))
println(shapes.map(area))
}
} Erlang models "one of several shapes" as differently-shaped tagged tuples, distinguished by their first element and arity at match time. Scala's
sealed trait Shape with case-class subtypes makes this a genuine closed sum type — sealed means the compiler knows every possible subtype lives in this file, and can warn if a match misses one, where Erlang would only discover a missed tuple shape when that exact input finally arrives at runtime.Atoms vs. case objects
Status = ok,
io:format("~p~n", [Status]). sealed trait Status
case object Ok extends Status
case object Failed extends Status
object Main {
def main(args: Array[String]): Unit = {
val status: Status = Ok
println(status)
}
} An Erlang atom like
ok is an unstructured constant drawn from a global, unbounded namespace of possible atoms. Scala's case object Ok extends Status gives the same "one fixed named value" idea but scoped and typed — a Status can only ever be Ok or Failed, never some unrelated value that happened to be spelled the same way elsewhere in the program.Lists & Collections
Lists and the cons pattern
[Head | Tail] = [1, 2, 3],
io:format("~p and ~p~n", [Head, Tail]). object Main {
def main(args: Array[String]): Unit = {
val head :: tail = List(1, 2, 3)
println(s"$head and $tail")
}
} Both languages model lists as singly-linked cons cells. Erlang spells the pattern
[Head | Tail]; Scala spells it head :: tail, using the same :: operator that List itself is built from. Scala lists must be homogeneous (one element type throughout), where Erlang lists happily mix atoms, numbers, and tuples.Map and filter
Doubled = lists:map(fun(N) -> N * 2 end, [1, 2, 3, 4, 5]),
Evens = lists:filter(fun(N) -> N rem 2 =:= 0 end, Doubled),
io:format("~p~n", [Evens]). object Main {
def main(args: Array[String]): Unit = {
val doubled = List(1, 2, 3, 4, 5).map(_ * 2)
val evens = doubled.filter(_ % 2 == 0)
println(evens)
}
} Both languages call these
map and filter. The syntax differs — Erlang passes the collection as an argument (lists:map(Fun, List)), Scala calls the method directly on the collection (list.map(fun)) — but the underlying transformation is identical, and _ * 2 is Scala's terse placeholder syntax for a one-argument lambda.Folding a list
Total = lists:foldl(fun(N, Accumulator) -> N + Accumulator end, 0, [1, 2, 3, 4, 5]),
io:format("~p~n", [Total]). object Main {
def main(args: Array[String]): Unit = {
val total = List(1, 2, 3, 4, 5).foldLeft(0)((accumulator, n) => accumulator + n)
println(total)
}
} Erlang's
lists:foldl/3 takes (function, initial accumulator, list); Scala's foldLeft is called on the list itself and takes (initial accumulator, function), with the accumulator listed first inside the lambda rather than the element. Same reduction, different argument order to watch for when porting code by hand.List Comprehensions vs. For
A comprehension becomes a `for`-`yield`
Squares = [X * X || X <- [1, 2, 3, 4, 5]],
io:format("~p~n", [Squares]). object Main {
def main(args: Array[String]): Unit = {
val squares = for (x <- List(1, 2, 3, 4, 5)) yield x * x
println(squares)
}
} Erlang's
[Expr || Generator] list comprehension and Scala's for (generator) yield expr comprehension both describe "produce this expression for every value the generator yields." The keyword yield at the end is Scala's equivalent of Erlang putting the expression up front — both mark the same collected result.Adding a filter condition
Evens = [X || X <- lists:seq(1, 10), X rem 2 =:= 0],
io:format("~p~n", [Evens]). object Main {
def main(args: Array[String]): Unit = {
val evens = for (x <- 1 to 10 if x % 2 == 0) yield x
println(evens.toList)
}
} Erlang appends the filter condition after a comma inside the same brackets; Scala's
for attaches it with an if guard inside the parentheses. Both filter the generator before applying the result expression, and both read left-to-right as "for each X in this range, where this holds, produce this."Multiple generators (a cartesian join)
Pairs = [{X, Y} || X <- [1, 2], Y <- [a, b]],
io:format("~p~n", [Pairs]). object Main {
def main(args: Array[String]): Unit = {
val pairs = for {
x <- List(1, 2)
y <- List("a", "b")
} yield (x, y)
println(pairs)
}
} Multiple generators produce every combination in both languages, with the last generator varying fastest. Scala's multi-line
for { ... } block form (using braces instead of a single line) is the idiomatic style once more than one generator is involved — worth recognizing as the same construct, not a different feature.Recursion
Factorial
Factorial = fun
Factorial(0) -> 1;
Factorial(N) when N > 0 -> N * Factorial(N - 1)
end,
io:format("~p~n", [Factorial(5)]). object Main {
def factorial(n: Int): Int =
if (n == 0) 1 else n * factorial(n - 1)
def main(args: Array[String]): Unit = {
println(factorial(5))
}
} Erlang expresses the base case and recursive case as separate clauses; Scala often expresses the same idea as a single
if/else expression, since Scala's match (the closer syntactic parallel to Erlang clauses) is more commonly reserved for genuine pattern matching rather than simple arithmetic conditions. Either style computes the identical result.Tail recursion with `@tailrec`
SumList = fun
SumList([], Accumulator) -> Accumulator;
SumList([Head | Tail], Accumulator) -> SumList(Tail, Head + Accumulator)
end,
io:format("~p~n", [SumList([1, 2, 3, 4, 5], 0)]). import scala.annotation.tailrec
object Main {
@tailrec
def sumList(numbers: List[Int], accumulator: Int): Int = numbers match {
case Nil => accumulator
case head :: tail => sumList(tail, head + accumulator)
}
def main(args: Array[String]): Unit = {
println(sumList(List(1, 2, 3, 4, 5), 0))
}
} The accumulator-passing style every Erlang developer already relies on for stack-safe recursion transfers directly. Scala adds one thing Erlang has no equivalent for: the
@tailrec annotation, which makes the compiler verify the function is actually tail-recursive and refuse to compile if it is not — Erlang's BEAM optimizes tail calls silently, with no way to ask the compiler to confirm a given function qualifies.Higher-Order Functions & Currying
Functions as values
Apply = fun(Function, Value) -> Function(Value) end,
Increment = fun(X) -> X + 1 end,
io:format("~p~n", [Apply(Increment, 41)]). object Main {
def apply(function: Int => Int, value: Int): Int = function(value)
def main(args: Array[String]): Unit = {
val increment = (x: Int) => x + 1
println(apply(increment, 41))
}
} Both languages treat functions as ordinary values — Erlang
funs and Scala function values are equally first-class. Scala's Int => Int is a function-type annotation, spelling out in the type system something Erlang leaves entirely implicit.Currying needs an extra parameter list
% Erlang funs have a fixed arity — building a partial application means
% wrapping it in a new fun by hand:
Add = fun(X, Y) -> X + Y end,
AddFive = fun(Y) -> Add(5, Y) end,
io:format("~p~n", [AddFive(10)]). object Main {
def add(x: Int)(y: Int): Int = x + y
def main(args: Array[String]): Unit = {
val addFive = add(5) _
println(addFive(10))
}
} Erlang requires writing the wrapping
fun by hand to get a partial application, exactly as shown. Scala supports currying natively via multiple parameter lists — def add(x: Int)(y: Int) — and add(5) _ (the trailing underscore) explicitly requests the remaining function as a value. Unlike Haskell, Scala functions are not curried automatically; you opt in with multiple parameter lists when you want this.Immutability: val vs. Single Assignment
`val` is close, but not identical, to Erlang binding
X = 40,
% X = 41 would be a {badmatch,41} error — Erlang variables bind once, period:
io:format("~p~n", [X]). object Main {
def main(args: Array[String]): Unit = {
val x = 40
// x = 41 below would be a compile error: "reassignment to val"
println(x)
}
} Erlang variables genuinely cannot be rebound within a scope — it is a match failure, enforced by the language itself, everywhere. Scala's
val gives the same guarantee but as a choice: Scala also has var for genuine mutable variables, so immutability is a discipline you (or your team's style guide) opt into, not a rule the language forces on every binding the way Erlang does.Scala also allows real mutation — Erlang never does
% Erlang has no mutable variable at all — "changing" a value always
% means creating a NEW binding under a new name, or recursing with a
% new argument (see the accumulator pattern in Recursion):
Count = 0,
NewCount = Count + 1,
io:format("~p~n", [NewCount]). object Main {
def main(args: Array[String]): Unit = {
var count = 0
count = count + 1
println(count)
}
} This is a genuine capability gap, not just a style choice: Scala's
var is real, in-place mutation of a variable, something no Erlang construct can do — even Erlang's process state is really "a new recursive call with a new argument," never a variable changing value under your feet. Idiomatic Scala reaches for var sparingly (favoring val and immutable collections almost everywhere), but the option exists in a way it structurally cannot in Erlang.Actors: Erlang Processes and Akka
Akka was explicitly modeled on Erlang/OTP
% Erlang: spawn a lightweight, isolated process and send it a message —
self() ! {greeting, "hello"},
receive
{greeting, Message} -> io:format("~s~n", [Message])
end. // Akka (not shown running here — this illustrates the parallel API shape):
// import akka.actor.typed.scaladsl.Behaviors
// val greeter = Behaviors.receiveMessage[String] { message =>
// println(message)
// Behaviors.same
// }
object Main {
def main(args: Array[String]): Unit = {
println("hello")
}
} This is the headline historical parallel of this whole comparison: Akka, the JVM's most widely used actor framework, was explicitly built as "Erlang-style actors for Scala/Java" — mailboxes, message passing, and supervision hierarchies are direct analogues of Erlang's process model, spawn/send/receive, and OTP supervisors. The commented Akka snippet is not executed (Akka is a separate library dependency this sandbox does not vendor), but the shape — receive a message, react, continue — is the same shape Erlang's
receive block already uses."Let it crash" has a direct Akka equivalent
% Erlang: a crashed process is restarted by its supervisor into
% a known-good state — the crash itself is not the problem:
try 1 div 0 of
Result -> io:format("~p~n", [Result])
catch
error:badarith -> io:format("crashed — a supervisor would restart me~n")
end. object Main {
def main(args: Array[String]): Unit = {
// Akka's SupervisorStrategy.restart plays the same role as an
// Erlang supervisor's {restart, permanent} child spec — restart
// the failed actor into a fresh, known-good state.
try {
println(1 / 0)
} catch {
case _: ArithmeticException =>
println("crashed — a supervisor would restart me")
}
}
} Erlang's "let it crash" philosophy is not folklore borrowed loosely by Akka — Akka's
SupervisorStrategy (restart, resume, stop, escalate) is a near-literal port of OTP's supervisor restart strategies (permanent, transient, temporary). An Erlang developer reading Akka supervision code will recognize the vocabulary, not just the concept.Gotchas for Erlang Developers
Reference equality is not the JVM default trap here — but watch collections
Left = [1, 2, 3],
Right = [1, 2, 3],
io:format("~p~n", [Left =:= Right]). object Main {
def main(args: Array[String]): Unit = {
val left = List(1, 2, 3)
val right = List(1, 2, 3)
println(left == right)
}
} Erlang's
=:= always compares by structure and value, term for term — there is no other notion of equality to worry about. Scala's == is, reassuringly, also structural for case classes and standard collections like List (unlike raw Java's ==, which compares references) — but this is a deliberate override Scala's standard library provides, not a universal language guarantee the way it is in Erlang.`null` still exists, unlike in Erlang
% Erlang has no null — the closest equivalent is the atom undefined,
% which is just an ordinary atom with no special language treatment:
Value = undefined,
io:format("~p~n", [Value]). object Main {
def main(args: Array[String]): Unit = {
val value: String = null // legal, but considered bad practice
println(Option(value)) // Option(null) becomes None
}
} Erlang has no
null concept whatsoever — undefined is just a regular atom with no compiler support behind it. Scala, inheriting the JVM, still has a real null that can be assigned to any reference type; idiomatic Scala avoids it almost entirely in favor of Option, and Option(value) is the standard bridge for wrapping a possibly-null Java API result safely.Build tools: rebar3/Mix vs. sbt
% rebar3 (Erlang's build tool):
% rebar3 new app myapp — create new project
% rebar3 compile — compile
% rebar3 shell — start a shell with the project loaded
% rebar3 eunit — run tests
% rebar3 release — build a deployable release // sbt (Scala's build tool):
// sbt new scala/scala-seed.g8 — create new project from a template
// sbt compile — compile
// sbt console — start a REPL with the project loaded
// sbt test — run tests
// sbt assembly — build a deployable fat JAR (via a plugin) The two ecosystems map closely at the command level: creating a project, compiling, an interactive shell, running tests, and producing a deployable artifact are all one
rebar3/sbt subcommand away. sbt's build file is Scala itself (build.sbt), where rebar3's is Erlang terms (rebar.config) — both let the build configuration be written in a dialect of the language it builds.