PONY λ M2 Modula-2

Erlang.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Erlang and Ruby

Erlang/OTP 26 Ruby 4.0
Output & Running
Hello, World
io:format("Hello, World!~n").
puts "Hello, World!"
Erlang prints with io:format/1 and the ~n directive for the newline. Ruby's puts writes its argument followed by a newline automatically, and needs no format string at all for a plain string.
Printing a value for inspection
Value = {ok, [1, 2, 3]}, io:format("~p~n", [Value]).
value = [:ok, [1, 2, 3]] p value
Erlang's ~p ("pretty print") directive renders any term for humans. Ruby's p method calls .inspect on its argument and prints the result — a close match for the same "show me this value, structure and all" need.
Dynamic Typing: a Shared Trait
Both languages check types only at runtime
Add = fun(X, Y) -> X + Y end, io:format("~p~n", [Add(2, 3)]).
add = ->(x, y) { x + y } puts add.call(2, 3)
This is a rarer pairing on this site: unlike the Haskell, OCaml, Rust, F#, or Scala comparisons — where the headline contrast is always dynamic-versus-static — Ruby is dynamically typed exactly like Erlang. Neither language declares a type anywhere here, and both would only discover add.call("two", 3) is a problem the moment that exact line actually runs.
A type mismatch is a runtime error in both
try 1 + "not a number" catch error:badarith -> io:format("Erlang: arithmetic error~n") end.
begin 1 + "not a number" rescue TypeError puts "Ruby: TypeError" end
Neither the Erlang badarith nor Ruby's TypeError is caught until the offending line actually executes — no compiler exists in either language to reject this ahead of time, the shared trait that sets this pairing apart from most others on the site.
Atoms vs. Symbols: a Close Match
Erlang atoms and Ruby symbols are the same idea
Status = ok, io:format("~p~n", [Status]).
status = :ok p status
This is one of the closest single-concept matches anywhere on the site: an Erlang atom like ok and a Ruby symbol like :ok are the same idea under two names — a lightweight, immutable, interned name that compares by identity rather than by copying characters, unlike a full string.
Using atoms/symbols as map/hash keys
Person = #{name => "Ada", role => engineer}, io:format("~p~n", [maps:get(role, Person)]).
person = { name: "Ada", role: :engineer } puts person[:role]
Both languages favor atoms/symbols as map keys over plain strings, for the same reason: they are cheap to compare and cheap to store since equal atoms/symbols are actually the same object in memory. Ruby's name: shorthand in a hash literal is exactly name: :name — key and value both drawing from the same symbol namespace as the identifier.
Pattern Matching vs. case/in
Function clauses become a `case/in` pattern match
Describe = fun Describe(0) -> "zero"; Describe(N) when N > 0 -> "positive"; Describe(_) -> "negative" end, io:format("~s~n", [Describe(-5)]).
describe = ->(n) { case n in 0 then "zero" in Integer if n > 0 then "positive" else "negative" end } puts describe.call(-5)
Ruby only gained genuine pattern matching in version 2.7 with case/in — for most of Ruby's history this comparison would have had no equivalent at all. Note Ruby spells its guard clause if, not when like Erlang (Ruby already uses when for the older, simpler case/when form), one of the few naming mismatches in an otherwise close comparison.
Destructuring a tuple/array
Result = {ok, 42}, {ok, Value} = Result, io:format("~p~n", [Value]).
result = [:ok, 42] _, value = result puts value
Both languages destructure a fixed-shape sequence directly in a single assignment. Ruby has no separate tuple type — the closest structural equivalent is a plain Array, destructured with ordinary multiple assignment.
Tagged Tuples vs. Objects
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]).
class Circle def initialize(radius) = @radius = radius def area = 3.14159 * @radius ** 2 end class Rectangle def initialize(width, height) = (@width, @height = width, height) def area = @width * @height end shapes = [Circle.new(5), Rectangle.new(3, 4)] areas = shapes.map(&:area) p areas
Erlang models "one of several shapes" as differently-tagged tuples, distinguished by their first element and arity. Ruby, being relentlessly object-oriented, instead gives each shape its own class with its own area method — polymorphic dispatch replaces pattern matching on a tag entirely, the biggest structural difference in this whole comparison.
Duck typing replaces tag-based dispatch
Speak = fun ({dog, Name}) -> io_lib:format("~s says Woof!", [Name]); ({cat, Name}) -> io_lib:format("~s says Meow!", [Name]) end, io:format("~s~n", [Speak({dog, "Rex"})]).
class Dog def initialize(name) = @name = name def speak = "#{@name} says Woof!" end class Cat def initialize(name) = @name = name def speak = "#{@name} says Meow!" end puts Dog.new("Rex").speak
Ruby needs no shared base class or interface here — anything that responds to .speak works wherever a .speak-calling method expects it. This is "duck typing," and it plays the same role Erlang's pattern-matched tags play: dispatching different behavior for different shapes of data, just organized around methods on objects instead of matched tuples.
{ok/error} vs. Exceptions
{ok, X}/{error, Reason} becomes raise/rescue
Divide = fun(_, 0) -> {error, divide_by_zero}; (X, Y) -> {ok, X div Y} end, case Divide(10, 0) of {ok, Value} -> io:format("~p~n", [Value]); {error, Reason} -> io:format("Error: ~p~n", [Reason]) end.
def divide(x, y) raise ZeroDivisionError, "divide by zero" if y == 0 x / y end begin puts divide(10, 0) rescue ZeroDivisionError => error puts "Error: #{error.message}" end
Erlang's convention of returning a tagged {error, Reason} tuple and matching on it is a value-based approach to failure. Ruby instead raises a real exception object and unwinds the call stack until a rescue catches it — the two languages solve "how do I signal failure" in fundamentally different ways, one of the bigger structural gaps in this comparison.
Skipping the failure case: different consequences
% If Divide's caller only matches {ok, _} and never {error, _}, a % failure case produces a "no matching clause" crash right there: Divide = fun(_, 0) -> {error, divide_by_zero}; (X, Y) -> {ok, X div Y} end, {ok, Value} = Divide(10, 2), io:format("~p~n", [Value]).
def divide(x, y) raise ZeroDivisionError, "divide by zero" if y == 0 x / y end # If the caller never wraps this in begin/rescue, an exception # propagates all the way up and crashes the whole program the # same way an unmatched Erlang clause does: puts divide(10, 2)
Neither language forces you to handle a failure path — an Erlang clause that only matches {ok, _} crashes with {badmatch, _} on an unhandled {error, _}, and an un-rescued Ruby exception crashes the whole process the same way. Both languages trust the programmer here rather than a compiler.
Immutable-Only vs. Pervasive Mutation
A real capability gap: Ruby permits pervasive mutation
% Erlang has no mutable variable at all — "changing" a value always % means a new binding: Count = 0, NewCount = Count + 1, io:format("~p~n", [NewCount]).
count = 0 count += 1 puts count
This is a genuine capability gap running the opposite direction from most Erlang comparisons: Ruby variables are ordinary mutable references, freely reassigned with = or +=. Erlang has no equivalent whatsoever — a variable, once bound, can never be rebound in the same scope; "changing" a value always means introducing a brand-new name.
Arrays mutate in place; Erlang lists never do
% Erlang: "appending" to a list always builds an entirely new list — % the original [1, 2, 3] is untouched and still exists: Original = [1, 2, 3], Updated = Original ++ [4], io:format("~p and ~p~n", [Original, Updated]).
original = [1, 2, 3] original << 4 puts original.inspect
Ruby's << mutates the array itself, in place, with no new object created — genuinely surprising for an Erlang developer, since no Erlang list can ever be changed after creation. Every "modification" in Erlang, no exceptions, produces a new value while leaving the original untouched.
Lists vs. Arrays
Lists and the cons pattern
[Head | Tail] = [1, 2, 3], io:format("~p and ~p~n", [Head, Tail]).
head, *tail = [1, 2, 3] puts "#{head} and #{tail}"
Erlang spells "first element, rest of the list" as [Head | Tail]. Ruby's splat operator *tail in a multiple assignment captures the same idea, though Ruby's Array is backed by a contiguous, resizable buffer rather than Erlang's singly-linked cons cells — indexing a Ruby array is O(1), indexing deep into an Erlang list is O(n).
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]).
doubled = [1, 2, 3, 4, 5].map { |n| n * 2 } evens = doubled.select { |n| n.even? } p evens
Both languages call the transform map. Erlang's filter is lists:filter/2; Ruby's idiomatic name is select (filter also exists as an alias). Ruby's block syntax ({ |n| ... }) reads noticeably closer to natural English than Erlang's explicit fun ... end.
Folding/reducing a list
Total = lists:foldl(fun(N, Accumulator) -> N + Accumulator end, 0, [1, 2, 3, 4, 5]), io:format("~p~n", [Total]).
total = [1, 2, 3, 4, 5].reduce(0) { |accumulator, n| accumulator + n } puts total
Erlang's lists:foldl/3 takes (function, initial accumulator, list); Ruby's reduce (alias inject) takes (initial accumulator, block) with the list as the receiver — reordered, but the same three ingredients, and Ruby even allows dropping the initial value entirely when the first element can serve as it.
List Comprehensions vs. Enumerable
No native comprehension syntax in Ruby either
Squares = [X * X || X <- [1, 2, 3, 4, 5]], io:format("~p~n", [Squares]).
squares = [1, 2, 3, 4, 5].map { |n| n * n } p squares
Erlang's [Expr || Generator] comprehension syntax (borrowed directly from Haskell) has no equivalent bracket-based syntax in Ruby — the same transformation is always spelled out as a chained Enumerable method call. Ruby's Enumerable module, mixed into every collection, plays the role a comprehension would, just through ordinary method calls rather than special syntax.
A filtered comprehension becomes a chained select/map
Evens = [X || X <- lists:seq(1, 10), X rem 2 =:= 0], io:format("~p~n", [Evens]).
evens = (1..10).select { |n| n.even? } p evens
Erlang appends the filter condition after a comma inside the same comprehension brackets. Ruby chains .select onto a Range instead — noticeably close in length and readability to the Erlang original, even without dedicated comprehension syntax, since Ruby's block-based methods read fluently on their own.
Recursion vs. Idiomatic Iteration
Factorial: recursion is idiomatic in Erlang, iteration in Ruby
Factorial = fun Factorial(0) -> 1; Factorial(N) when N > 0 -> N * Factorial(N - 1) end, io:format("~p~n", [Factorial(5)]).
def factorial(n) = (1..n).reduce(1, :*) puts factorial(5)
Erlang has no loop constructs at all — recursion is the *only* way to repeat, so a base-case-plus-recursive-case function is the everyday idiom. Ruby offers real loops and, more idiomatically, Enumerable methods like reduce — an experienced Rubyist reaches for iteration over explicit recursion far more often than an Erlang developer would.
Ruby does not guarantee tail-call optimization
% Erlang/BEAM formally guarantees this runs in constant stack % space, no matter how large N is: 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)]).
def sum_list(numbers, accumulator = 0) return accumulator if numbers.empty? sum_list(numbers[1..], accumulator + numbers[0]) end # The standard MRI interpreter does NOT guarantee tail-call # optimization by default — a sufficiently long list here risks a # SystemStackError that the equivalent Erlang code never could. puts sum_list([1, 2, 3, 4, 5])
This is a real reliability gap worth knowing about. Erlang and the BEAM formally guarantee last-call optimization, so an accumulator-passing recursive function like this always runs in constant stack space. Ruby's standard implementation (MRI) does not enable tail-call optimization by default, so an equivalent deeply-recursive Ruby function risks overflowing the stack where the Erlang version never would — idiomatic Ruby reaches for iteration specifically to sidestep this.
Funs vs. Blocks, Procs & Lambdas
Functions/blocks as values
Apply = fun(Function, Value) -> Function(Value) end, Increment = fun(X) -> X + 1 end, io:format("~p~n", [Apply(Increment, 41)]).
apply = ->(callable, value) { callable.call(value) } increment = ->(x) { x + 1 } puts apply.call(increment, 41)
Both languages treat callable values as ordinary first-class objects. Ruby actually offers three closely related flavors here — blocks, Proc.new, and lambda/-> — where Erlang has just one: the fun. Ruby's lambdas are the closest match, since (like Erlang funs) they enforce strict arity and a return only exits the lambda itself.
Currying needs explicit help in both languages
% 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)]).
add = ->(x, y) { x + y } add_five = add.curry[5] puts add_five.call(10)
Neither language curries automatically the way OCaml or Haskell do. Erlang requires wrapping a partial application in a brand-new fun by hand. Ruby's Proc#curry method does the wrapping for you, given an explicit opt-in call — closer to Erlang's "you must ask for it" stance than to a language where currying is simply how every function already behaves.
Records vs. Struct
-record becomes Ruby's Struct
% Erlang records are compile-time sugar for tagged tuples: Point = {point, 3, 4}, {point, X, Y} = Point, io:format("~p and ~p~n", [X, Y]).
Point = Struct.new(:x, :y) point = Point.new(3, 4) puts "#{point.x} and #{point.y}"
Erlang's -record(point, {x, y}). desugars to a tagged tuple, a naming convenience only. Ruby's Struct.new creates a genuine lightweight class with named accessors — closer in spirit to a real record than Erlang's, since it is a distinct object type rather than an indistinguishable tuple shape.
Ruby's Struct fields are mutable — Erlang's never are
% Erlang: nothing can be mutated in place, so "updating" a record % field always means rebuilding the whole tuple: Point = {point, 3, 4}, {point, X, _} = Point, NewPoint = {point, X, 99}, io:format("~p~n", [NewPoint]).
Point = Struct.new(:x, :y) point = Point.new(3, 4) point.y = 99 puts "#{point.x} and #{point.y}"
Ruby's Struct fields can be reassigned directly with a setter — point.y = 99 mutates the same object in place. There is no equivalent whatsoever in Erlang: every record field is immutable forever, so "updating" one always means constructing an entirely new tuple, as the previous example showed.
Modules: Same Word, Different Job
`module` means something different in each language
% Erlang: a module is just a namespace for functions — nothing % can be "mixed in" to anything else: io:format("~p~n", [erlang_module_is_a_namespace]).
module Greetable def greet = "Hello, #{name}!" end class Person include Greetable attr_reader :name def initialize(name) = @name = name end puts Person.new("Ada").greet
The word "module" means genuinely different things in each language. An Erlang module is purely a namespace boundary for grouping functions in one file — it cannot be attached to anything. A Ruby module can also be included directly into a class as a mixin, injecting its methods as if they were defined on that class — a capability with no Erlang equivalent at all.
Processes vs. Threads: a Real Downgrade
Erlang processes have no real Ruby Thread equivalent
% Erlang: spawn a fully isolated, independently-collected process % and pass it a message — this is why Erlang exists in the first % place, and no Erlang program ever needs to think about locks: Pid = spawn(fun() -> receive {greet, Name} -> io:format("Hello, ~s!~n", [Name]) end end), Pid ! {greet, "Ada"}.
queue = Queue.new thread = Thread.new do name = queue.pop puts "Hello, #{name}!" end queue << "Ada" thread.join
This is a genuine downgrade worth being honest about, not glossing over: Erlang processes are lightweight (millions per node), fully isolated with their own heap and garbage collector, and communicate only by copying messages — the entire reason Erlang/OTP exists. Ruby's Thread shares memory directly and, in the standard MRI implementation, runs under a Global VM Lock that prevents more than one thread from executing Ruby code at the same instant — nothing in Ruby offers Erlang-style isolation, supervision trees, or "let it crash" recovery out of the box.
Shared memory needs an explicit Mutex — Erlang needs none
% Erlang: two processes can NEVER corrupt each other's state, % because they share no memory to corrupt — there is structurally % nothing to protect: Counter = 0, NewCounter = Counter + 1, io:format("~p~n", [NewCounter]).
require "thread" counter = 0 mutex = Mutex.new threads = 5.times.map do Thread.new { mutex.synchronize { counter += 1 } } end threads.each(&:join) puts counter
Because Ruby threads share one heap, incrementing a shared counter from several threads at once needs an explicit Mutex to avoid a lost update — a category of bug that cannot occur in Erlang at all, since no two Erlang processes ever share mutable memory to race over in the first place.
Exceptions: a Shared, Comfortable Tool
try/catch and begin/rescue — both reach for exceptions readily
try 1/0 catch error:badarith -> io:format("Caught division error~n") end.
begin 1 / 0 rescue ZeroDivisionError puts "Caught division error" end
Both Erlang and Ruby reach for genuine exceptions comfortably and often, unlike the Haskell/OCaml/Rust family of languages that tend to prefer Either/Result for everyday failures. try ... catch and begin ... rescue are both everyday, idiomatic tools in these languages.
Declaring and raising a custom exception
try throw({invalid_age, -1}) catch throw:{invalid_age, Age} -> io:format("Invalid age: ~p~n", [Age]) end.
class InvalidAgeError < StandardError; end begin raise InvalidAgeError, "-1" rescue InvalidAgeError => error puts "Invalid age: #{error.message}" end
Erlang has no separate exception-declaration step at all — throw({invalid_age, -1}) just throws an ordinary tagged tuple, caught by pattern matching like any other value. Ruby requires declaring a real class that inherits from StandardError (or one of its subclasses) — a genuine, nominal exception type the rescue clause matches by class, not by shape.
Gotchas for Erlang Developers
A "string" means something different again
% Erlang: "hello" is secretly a LIST of character codes: Value = "hello", io:format("~w~n", [Value]). % shows [104,101,108,108,111]
value = "hello" # a real String object, with dozens of built-in # methods, not a list of character codes at all puts value.class
Erlang's "hello" is secretly [104, 101, 108, 108, 111], a plain list of integer code points — which is why ~w ("write the raw term") shows the numbers. Ruby's "hello" is a genuine String object with its own rich method set (.upcase, .split, .reverse, ...) — much closer to how most other languages on this site treat strings than Erlang's code-point-list representation is.
What counts as "false" differs
% Erlang has no dedicated boolean type at all — true and false % are just ordinary atoms, and only the atom false (never 0, never % an empty list) is falsy in a boolean context: Value = false, io:format("~p~n", [Value =:= false]).
# Ruby has a real boolean type, but ONLY false and nil are falsy — # 0 and "" and [] are all truthy, unlike many other dynamic languages: value = 0 puts value ? "truthy" : "falsy"
Erlang has no dedicated boolean type — true and false are ordinary atoms, and only the atom false is ever falsy in a guard or case. Ruby has a genuine true/false type, but its falsy set is false and nil only — 0, "", and [] are all truthy, a common trip-up for developers coming from languages (including many with C-like roots) where zero or empty means false.