PONY λ M2 Modula-2

Elixir.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Elixir and Ruby

Elixir 1.17 Ruby 4.0
The Familiar Surface
Hello, World
IO.puts("Hello, World!")
puts "Hello, World!"
Ruby’s puts needs no module prefix — and no parentheses, which idiomatic Ruby omits when a call reads like a command. Much of what feels familiar on this page is not coincidence: Elixir deliberately borrowed Ruby’s surface syntax.
Interpolation & %w — shared heritage
name = "Ada" count = 3 IO.puts("#{name} has #{count} items") words = ~w(alpha beta gamma) IO.inspect(words)
name = "Ada" count = 3 puts "#{name} has #{count} items" words = %w[alpha beta gamma] p words
The #{} interpolation syntax is identical because Elixir took it from Ruby — and Elixir’s ~w sigil is Ruby’s %w word-array literal, generalized. p is the everyday IO.inspect: it prints the value’s literal representation and returns it.
Atoms → symbols
status = :active IO.inspect(status) IO.inspect(status == :active) IO.inspect(%{name: "Ada", role: :admin})
status = :active p status p status == :active p({ name: "Ada", role: :admin })
Symbols are atoms under another name — lightweight, interned, identity-compared constants, used for the same jobs: statuses, option keys, hash/map keys. Even the shorthand map/hash literal with key: labels looks the same.
Everything Is an Object
Behavior moves onto the data
greeting = "hello world" IO.puts(String.upcase(greeting)) IO.puts(String.length(greeting)) IO.inspect(String.split(greeting, " "))
greeting = "hello world" puts greeting.upcase puts greeting.length p greeting.split(" ")
The fundamental inversion of this whole page: in Elixir, functions live in modules and data is passed in; in Ruby, methods live on the object. String.upcase(greeting) becomes greeting.upcase — the receiver comes first, and there is no module to name.
Even literals receive messages
# Elixir integers are plain values — Integer functions take them # as arguments, and repetition is a range + Enum. IO.inspect(abs(-3)) Enum.each(1..2, fn _index -> IO.puts("again") end) IO.puts(nil == nil)
p(-3.abs) # integers are objects with methods 2.times { puts "again" } p nil.to_s # even nil is an object (NilClass) p 5.class p nil.class
There are no plain values: integers, nil, true, classes themselves — everything is an object that responds to methods. 2.times reads oddly for about a day and then becomes the most natural loop you know.
defstruct → class
defmodule Person do defstruct [:name, :age] def greeting(%Person{name: name}) do "Hello, #{name}!" end end person = %Person{name: "Ada", age: 36} IO.puts(Person.greeting(person))
class Person attr_reader :name, :age def initialize(name:, age:) @name = name @age = age end def greeting = "Hello, #{@name}!" end person = Person.new(name: "Ada", age: 36) puts person.greeting
A struct plus its module of functions becomes a class holding both state and behavior. Instance variables (@name) are private to the object — attr_reader generates the getter. Person.new allocates and calls initialize, and the one-line def greeting = … is Ruby 4’s endless method, a near-twin of def greeting, do: ….
Pattern-match dispatch → duck typing
# Dispatch on the SHAPE of the data, chosen at the function head: defmodule Describe do def call(%{type: :circle, radius: radius}), do: "circle r=#{radius}" def call(%{type: :square, side: side}), do: "square s=#{side}" end IO.puts(Describe.call(%{type: :circle, radius: 2})) IO.puts(Describe.call(%{type: :square, side: 3}))
class Circle def initialize(radius) = @radius = radius def describe = "circle r=#{@radius}" end class Square def initialize(side) = @side = side def describe = "square s=#{@side}" end [Circle.new(2), Square.new(3)].each do |shape| puts shape.describe # whatever responds to #describe, works end
Where Elixir picks a function clause by matching the data’s shape, Ruby picks a method by asking the object itself — polymorphic dispatch. Nothing declares the shared interface: any object that responds to describe qualifies ("if it quacks like a duck"). respond_to?(:describe) is the runtime check when you need one.
Mutation & Aliasing
Rebinding → real mutation
numbers = [1, 2, 3] more = numbers ++ [4] # a NEW list; numbers is untouched IO.inspect(numbers) IO.inspect(more)
numbers = [1, 2, 3] numbers << 4 # the SAME array, changed in place p numbers numbers.push(5).unshift(0) p numbers
Elixir rebinds names to new values; Ruby objects genuinely change in place. << appends to this array — no copy, no rebinding — and mutating methods chain because they return the receiver. Methods ending in ! (like sort!) conventionally flag the more surprising in-place variants.
Aliasing — the impossible bug
# This phenomenon cannot exist in Elixir: data is immutable, so # two names can never watch each other's edits. first = [1, 2, 3] second = first second = second ++ [4] # rebinds second only IO.inspect(first) # [1, 2, 3] — always IO.inspect(second)
first = [1, 2, 3] second = first # both names point at ONE array second << 4 p first # [1, 2, 3, 4] — first changed too! p second p first.equal?(second) # true — the very same object copied = first.dup copied << 5 p first # unaffected by the copy's edit
The bug class Elixir structurally deleted comes back: assignment copies the reference, so edits through one name are visible through every alias. equal? tests object identity, and dup makes the (shallow) copy that restores Elixir-style independence.
Ruby froze its strings
# Elixir strings (binaries) were always immutable: greeting = "hello" shouted = String.upcase(greeting) # a new binary IO.puts(greeting) IO.puts(shouted)
greeting = "hello" # greeting << " world" # FrozenError — string LITERALS are frozen in Ruby 4 p greeting.frozen? editable = +greeting # +string makes a mutable copy editable << " world" puts editable
Ruby moved a step toward Elixir: as of Ruby 4.0, string literals are frozen by default. Mutable strings still exist — +string or String.new produce unfrozen copies — but the default now matches the immutability Elixir never compromised on. Arrays and hashes remain mutable.
Opt-in immutability: freeze & Data
# Immutability is the only mode — no opt-in needed. point = %{x: 1, y: 2} moved = %{point | x: 99} # update syntax returns a NEW map IO.inspect(point) IO.inspect(moved)
Point = Data.define(:x, :y) # an immutable value class point = Point.new(x: 1, y: 2) moved = point.with(x: 99) # returns a NEW Point p point p moved settings = { theme: "dark" }.freeze p settings.frozen?
Ruby’s immutability is opt-in: freeze locks any object, and Data.define (Ruby 3.2+) creates genuinely immutable value classes whose with is Elixir’s %{struct | field: value} update. Idiomatic modern Ruby reaches for Data exactly where Elixir reaches for a struct.
= Is Just Assignment
No match operator, no pin
count = 1 count = count + 1 # rebinding IO.inspect(count) {:ok, value} = {:ok, 42} # = MATCHES — and can fail IO.inspect(value)
count = 1 count = count + 1 # plain reassignment — nothing is matched p count # There is no destructuring-with-failure: = never raises. # Multiple assignment is positional, and extra values are dropped: status, value = [:ok, 42] p [status, value]
Ruby’s = only assigns. Multiple assignment (a, b = list) destructures positionally but never fails — missing positions become nil, extras are discarded. The match-or-crash behavior of Elixir’s = has no equivalent outside case/in (next rows).
head | tail → splats
[head | tail] = [1, 2, 3, 4] IO.inspect(head) IO.inspect(tail)
head, *tail = [1, 2, 3, 4] p head p tail first, *middle, last = [1, 2, 3, 4, 5] p [first, middle, last]
The splat * collects the rest — Ruby’s head, *tail is Elixir’s [head | tail], and the splat can sit in the middle, something cons-cell patterns cannot express. Because Ruby arrays are not linked lists, none of this carries Elixir’s head-vs-tail performance asymmetry.
case/in — the real cousin
response = {:ok, %{name: "Ada", age: 36}} case response do {:ok, %{name: name}} when is_binary(name) -> IO.puts("hello, #{name}") {:error, reason} -> IO.puts("failed: #{inspect(reason)}") end
response = [:ok, { name: "Ada", age: 36 }] case response in [:ok, { name: String => name }] puts "hello, #{name}" in [:error, reason] puts "failed: #{reason}" end
Ruby 3 added real pattern matching, and it lives in case/in: array and hash patterns destructure, String => name both type-checks and binds (playing the role of Elixir’s when is_binary(name) guard), and an unmatched value raises NoMatchingPatternError — the match-or-crash semantics Elixir developers expect, opt-in.
nil & Truthiness
The same truthiness rule
# Only nil and false are falsy — everything else is truthy: IO.puts(if 0, do: "0 is truthy", else: "0 is falsy") IO.puts(if "", do: "empty string is truthy", else: "falsy") IO.puts(if nil, do: "truthy", else: "nil is falsy")
puts(0 ? "0 is truthy" : "0 is falsy") puts("" ? "empty string is truthy" : "falsy") puts(nil ? "truthy" : "nil is falsy")
A rule you already know, because Elixir inherited it from Ruby verbatim: only nil and false are falsy — 0 and "" are truthy, unlike most mainstream languages. Ruby adds a ternary operator Elixir lacks.
Handling absence
inventory = %{apples: 5} IO.inspect(inventory[:pears]) # nil for a missing key IO.inspect(Map.get(inventory, :pears, 0)) # with a default IO.inspect(inventory[:pears] || 0)
inventory = { apples: 5 } p inventory[:pears] # nil for a missing key p inventory.fetch(:pears, 0) # with a default p inventory[:pears] || 0 owner = nil p owner&.upcase # &. — safe navigation, nil if receiver is nil
The idioms rhyme (|| defaults, nil for missing keys, fetch as Map.get with a default) — plus &., the safe-navigation operator: where a nil receiver would raise NoMethodError, &. short-circuits to nil, one link at a time.
Blocks, Not fn
fn arguments → blocks
Enum.each([1, 2, 3], fn number -> IO.puts(number * 10) end)
[1, 2, 3].each do |number| puts number * 10 end [1, 2, 3].each { |number| puts number * 10 }
The block is Ruby’s signature construct: a closure passed outside the argument list, in do…end (multiline) or braces (one-liner), with parameters between pipes. It is not a value being passed — it is syntax every method can receive, and it is how all iteration works.
Writing methods that take blocks
defmodule Timer do def measure(work) do IO.puts("starting") result = work.() IO.puts("finished") result end end IO.inspect(Timer.measure(fn -> 6 * 7 end))
def measure puts "starting" result = yield # invoke the block the caller attached puts "finished" result end p(measure { 6 * 7 })
yield calls the block attached to the current method — no parameter is declared, no function object handled. block_given? tests for one, and an explicit &work parameter captures the block as a Proc when it must be stored or forwarded.
fn & capture → lambdas & &:
double = fn number -> number * 2 end IO.inspect(double.(21)) IO.inspect(Enum.map([1, 2, 3], &(&1 * 2))) IO.inspect(Enum.map(["a", "b"], &String.upcase/1))
double = ->(number) { number * 2 } p double.call(21) p double.(21) # same call, Elixir-style dot p [1, 2, 3].map { |number| number * 2 } p ["a", "b"].map(&:upcase) # &:symbol — the capture shorthand
When a closure must be a value, the lambda literal ->(x) { } is Elixir’s fn — and Ruby even accepts the .() call syntax Elixir requires. The &:upcase shorthand converts a symbol to a block, playing the role of &String.upcase/1.
No |> — chaining is the pipeline
"hello world elixir" |> String.split(" ") |> Enum.map(&String.capitalize/1) |> Enum.join(" ") |> IO.puts()
puts "hello world ruby" .split(" ") .map(&:capitalize) .join(" ") p 42.then { |number| number * 2 } # .then pipes a value into a block
Ruby has no pipe operator — and rarely misses it, because methods living on objects means every call already returns a receiver for the next one. .then covers the leftover case of piping a value into arbitrary code, the one-step |>.
Collections: Enum → Enumerable
Enum → Enumerable
[1, 2, 3, 4, 5, 6] |> Enum.filter(fn number -> rem(number, 2) == 0 end) |> Enum.map(fn number -> number * 10 end) |> Enum.sum() |> IO.inspect()
total = [1, 2, 3, 4, 5, 6] .select(&:even?) .map { |number| number * 10 } .sum p total
The vocabulary shifts more than the ideas: filter is select (with reject as its complement), reduce exists under both reduce and inject, and predicate methods end in ? (even?). All of it lives on the collection, so the chain needs no module names.
Maps → Hashes
inventory = %{apples: 5, pears: 2} updated = Map.put(inventory, :plums, 7) IO.inspect(updated[:apples]) IO.inspect(Map.keys(updated) |> Enum.sort()) nested = %{config: %{theme: "dark"}} IO.inspect(get_in(nested, [:config, :theme]))
inventory = { apples: 5, pears: 2 } inventory[:plums] = 7 # mutates in place, of course p inventory[:apples] p inventory.keys.sort nested = { config: { theme: "dark" } } p nested.dig(:config, :theme) # get_in
Hashes are maps you can mutate: hash[key] = value writes in place. dig is get_in, iteration order is insertion order (guaranteed, as in Elixir maps up to 32 keys — but here always), and symbol keys with the key: literal shorthand dominate real code just as atom keys do.
Stream → lazy
1..1_000_000 |> Stream.filter(fn number -> rem(number, 7) == 0 end) |> Stream.map(fn number -> number * 2 end) |> Enum.take(3) |> IO.inspect()
first_three = (1..1_000_000) .lazy .select { |number| (number % 7).zero? } .map { |number| number * 2 } .first(3) p first_three
.lazy converts any Enumerable into Elixir’s Stream: deferred, element-at-a-time evaluation where eager methods would materialize each intermediate array. A terminal call like first(3) plays the Enum.take/2 role of forcing the pipeline.
for comprehensions → chains
squares = for number <- 1..5, rem(number, 2) == 1 do number * number end IO.inspect(squares) pairs = for letter <- ~w(a b), number <- 1..2 do {letter, number} end IO.inspect(pairs)
squares = (1..5).select(&:odd?).map { |number| number * number } p squares pairs = %w[a b].product([1, 2]) p pairs
Ruby has no comprehension syntax; a filter-then-map chain covers the everyday case and product covers the multi-generator cross join. (Ruby’s for loop exists but is shunned — it leaks its variable into the surrounding scope, and everyone writes each instead.)
Methods
do: → endless def
defmodule Math do def square(number), do: number * number def cube(number), do: number * number * number end IO.inspect(Math.square(7)) IO.inspect(Math.cube(3))
def square(number) = number * number def cube(number) = number * number * number p square(7) p cube(3)
The languages converged from both ends: Elixir’s one-line def f(x), do: expr and Ruby 4’s endless method def f(x) = expr are near-twins. Ruby methods also need no enclosing module — top-level def is fine — and the last expression is the return value, just as in Elixir.
Keyword lists → real keyword arguments
# Options arrive as a keyword list — a plain list of tuples, # with defaults merged by hand: defmodule Resizer do def resize(width, height, options \\ []) do preserve = Keyword.get(options, :preserve_aspect, true) "#{width}x#{height} preserve=#{preserve}" end end IO.puts(Resizer.resize(800, 600)) IO.puts(Resizer.resize(800, 600, preserve_aspect: false))
def resize(width:, height:, preserve_aspect: true) "#{width}x#{height} preserve=#{preserve_aspect}" end puts resize(width: 800, height: 600) puts resize(width: 800, height: 600, preserve_aspect: false)
Ruby’s keyword arguments are a real language feature, not a trailing-list convention: width: with no default is required (omitting it raises ArgumentError), defaults sit in the signature, and no Keyword.get plumbing is needed. The call sites look identical — Elixir’s sugar was modeled on Ruby’s.
Guards & clauses → early returns
defmodule Classify do def call(number) when number < 0, do: "negative" def call(0), do: "zero" def call(number) when number > 0, do: "positive" end IO.puts(Classify.call(-5)) IO.puts(Classify.call(0)) IO.puts(Classify.call(9))
def classify(number) return "negative" if number.negative? return "zero" if number.zero? "positive" end puts classify(-5) puts classify(0) puts classify(9)
One method body replaces the clause list, and return — which Elixir deliberately lacks — handles the early exits, usually as trailing-if guard lines. The trailing conditional (return … if …) is idiomatic Ruby’s answer to when guards; unless is its negated sibling.
Mixins & Inheritance
Modules do two jobs
# An Elixir module is a namespace for functions — nothing more. defmodule Text.Formatter do def shout(text), do: String.upcase(text) <> "!" end IO.puts(Text.Formatter.shout("hello"))
module Text module Formatter def self.shout(text) = text.upcase + "!" end end puts Text::Formatter.shout("hello")
As a namespace, a Ruby module works like Elixir’s (with :: as the separator and self. marking module-level functions). But Ruby modules have a second job Elixir modules never do: being mixed into classes — the next row.
The mixin superpower
# The nearest Elixir gets is implementing a protocol per type — # but nothing hands you a pile of derived functions for free the # way Comparable does. (Runnable protocol definitions are also # unsupported on this page's AtomVM runtime.) defmodule Coffee do defstruct [:strength] def compare(%Coffee{strength: left}, %Coffee{strength: right}) do cond do left < right -> :lt left > right -> :gt true -> :eq end end end espresso = %Coffee{strength: 10} latte = %Coffee{strength: 3} IO.inspect(Coffee.compare(espresso, latte))
class Coffee include Comparable # implement <=>, inherit <, >, ==, between?, clamp… attr_reader :strength def initialize(strength) = @strength = strength def <=>(other) = strength <=> other.strength end espresso = Coffee.new(10) latte = Coffee.new(3) p espresso > latte p latte.between?(Coffee.new(1), Coffee.new(5)) p [espresso, latte].min.strength
Mixins are Ruby’s flagship reuse mechanism: include Comparable and one <=> definition buy every comparison operator; include Enumerable and one each buy map, select, sort, and dozens more. Elixir has no equivalent — protocols dispatch, but they do not donate implementations.
Inheritance exists
# Elixir has no inheritance — composition and delegation only: defmodule Animal do def speak(_animal), do: "..." end defmodule Dog do def speak(_dog), do: "Woof" def sniff(_dog), do: "sniffing" end IO.puts(Dog.speak(:rex)) IO.puts(Dog.sniff(:rex))
class Animal def speak = "..." def describe = "I say #{speak}" end class Dog < Animal def speak = "Woof" # overrides; describe is inherited end puts Dog.new.describe puts Animal.new.describe
Single inheritance (<) is ordinary Ruby: subclasses inherit and override methods, super calls up the chain, and inherited methods (like describe) see the subclass’s overrides — classic late binding. Modern Ruby style still prefers composition and mixins for sharing behavior across unrelated classes.
Runtime Metaprogramming
Open classes
# Elixir modules are closed at compile time. Adding a "method" # to String means defining your own module: defmodule StringExtras do def shout(text), do: String.upcase(text) <> "!" end IO.puts(StringExtras.shout("hello"))
class String # reopen the built-in class def shout = upcase + "!" end puts "hello".shout # every string everywhere now responds
Any class can be reopened and extended at runtime — including core classes, in what Rubyists call monkey-patching. It is how Rails makes 3.days.ago work. Powerful and dangerous in equal measure; Module#refine offers a lexically-scoped alternative when the blast radius matters.
method_missing — ghost methods
# Elixir metaprograms at COMPILE time with macros; there is no # runtime hook for calls to functions that do not exist — # an undefined function is just an error. defmodule Finder do def find_by(field, value), do: "SELECT * WHERE #{field} = '#{value}'" end IO.puts(Finder.find_by(:name, "Ada"))
class Finder def method_missing(name, *arguments) if name.to_s.start_with?("find_by_") field = name.to_s.delete_prefix("find_by_") "SELECT * WHERE #{field} = '#{arguments.first}'" else super end end def respond_to_missing?(name, include_private = false) name.to_s.start_with?("find_by_") || super end end puts Finder.new.find_by_name("Ada") # a method that was never defined
method_missing intercepts calls to undefined methods at runtime — the mechanism behind Rails’ classic dynamic finders. Where Elixir metaprograms ahead of time with macros generating real functions, Ruby can improvise the response the moment the message arrives.
define_method — generating methods at runtime
# Generating functions is a compile-time macro affair (use/quote). # The everyday stand-in: one function over data. defmodule Status do @statuses [:active, :archived] def status?(record_status, status) when status in @statuses do record_status == status end end IO.inspect(Status.status?(:active, :active)) IO.inspect(Status.status?(:active, :archived))
class Record [:active, :archived].each do |status| define_method("#{status}?") do @status == status end end def initialize(status) = @status = status end record = Record.new(:active) p record.active? # generated in the loop above p record.archived?
define_method writes real methods from ordinary runtime code — a loop over symbols here generates active? and archived?. This is the workhorse behind attr_accessor (itself just a method that defines methods) and most of Rails’ generated API.
Exceptions Are the Norm
Tagged tuples → raise/rescue
defmodule Parser do def parse(text) do case Integer.parse(text) do {value, ""} -> {:ok, value} _other -> {:error, "not a number: #{text}"} end end end case Parser.parse("42") do {:ok, value} -> IO.puts("parsed #{value}") {:error, reason} -> IO.puts(reason) end
def parse(text) Integer(text) # raises ArgumentError on bad input end begin puts "parsed #{parse("42")}" puts "parsed #{parse("many")}" rescue ArgumentError => error puts "failed: #{error.message}" end
The convention inverts: failure in Ruby normally raises, unwinding the stack until a rescue catches it — not a tagged tuple threaded through return values. begin/rescue/end is try/rescue, the error binds with =>, and a bare method body can rescue without the begin.
Custom errors & ensure
defmodule QuotaError do defexception message: "quota exceeded" end try do raise QuotaError rescue error in QuotaError -> IO.puts("caught: #{error.message}") after IO.puts("cleanup") end
class QuotaError < StandardError def initialize(message = "quota exceeded") = super end begin raise QuotaError rescue QuotaError => error puts "caught: #{error.message}" ensure puts "cleanup" end
A custom error is a class inheriting from StandardError (rescue’s default reach — inherit from it, not Exception). ensure is Elixir’s after: it runs on every exit. Ruby also has retry, which re-runs the whole begin block — a control-flow move Elixir deliberately omits.
The softer failure: nil returns
# Elixir splits these as find (nil) vs find! conventions too, # but the tagged tuple dominates library APIs: inventory = %{apples: 5} case Map.fetch(inventory, :pears) do {:ok, quantity} -> IO.puts("have #{quantity}") :error -> IO.puts("none in stock") end
inventory = { apples: 5 } # Two flavors, chosen by the CALLER: p inventory[:pears] # soft: nil when missing begin inventory.fetch(:pears) # hard: raises KeyError rescue KeyError => error puts "none in stock (#{error.class})" end
Ruby APIs conventionally come in pairs — a soft version returning nil ([], find) and a hard version raising (fetch, find! in Rails) — where Elixir would return {:ok, _} | :error and let the caller match. The !/fetch naming is the signal to watch for.
The Concurrency Downgrade
Processes → threads (shared memory, GVL)
# Each process has its OWN heap — the counter cannot be shared, # only messaged: counter = spawn(fn -> receive do {:add, amount, caller} -> send(caller, {:total, amount}) end end) send(counter, {:add, 5, self()}) receive do {:total, total} -> IO.puts("total #{total}") end
total = 0 workers = 2.times.map do Thread.new do total += 5 # threads SHARE memory — this is a race in general end end workers.each(&:join) puts "total #{total}"
The single biggest capability loss on this page: Ruby threads share one heap, so mutation from two threads is a data race the language does nothing to prevent — where BEAM processes are structurally isolated. In standard Ruby (MRI) a Global VM Lock serializes execution (this example is "safe" only by that accident), so threads help with I/O waiting, not CPU parallelism. On this site’s in-browser runtime, threads are faked synchronously.
Mailboxes → Queue
worker = spawn(fn -> receive do {:job, payload} -> IO.puts("processing #{payload}") end end) send(worker, {:job, "invoice-42"}) # Give the worker a beat to print before the script ends: receive do after 50 -> :ok end
jobs = Queue.new # a thread-safe FIFO worker = Thread.new do payload = jobs.pop # blocks until something arrives puts "processing #{payload}" end jobs.push("invoice-42") worker.join
Queue (thread-safe, blocking pop) is the closest stdlib analog to a process mailbox — but it is a shared object threads pull from, not a per-process inbox with selective receive. There is no supervision tree, no restart strategy, and no OTP: a crashed thread is simply gone unless you check on it.
Ractors — Ruby borrows the actor
# The model Ractor borrows from: isolated heaps, message passing. parent = self() spawn(fn -> send(parent, {:result, 6 * 7}) end) receive do {:result, value} -> IO.puts("got #{value}") end
# Ractor — Ruby's BEAM-inspired isolated actors (experimental): worker = Ractor.new do answer = Ractor.receive answer * 2 end worker.send(21) puts worker.value
Ruby is reaching toward the BEAM: Ractors (Ruby 3.0+, API revised in 3.5) get isolated object spaces, share almost nothing, communicate by message — and can run in true parallel, each holding its own lock. They remain experimental and library support is thin, so this cell is display-only: the in-browser runtime cannot run Ractors.
Tooling & Ecosystem
mix & Hex → Bundler & RubyGems
# mix.exs — project + dependencies in one file defp deps do [ {:jason, "~> 1.4"}, {:req, "~> 0.5"} ] end # $ mix deps.get && mix test
# Gemfile — dependencies only (project config lives elsewhere) source "https://rubygems.org" gem "json", "~> 2.7" gem "faraday", "~> 2.9" # $ bundle install && bundle exec rake test
The mapping is direct — Hex packages become gems, mix deps.get becomes bundle install, and the ~> pessimistic version operator is another thing Elixir took from Ruby unchanged. bundle exec pins commands to the Gemfile’s versions. Both cells are configuration fragments, shown display-only.
iex → irb
# $ iex # iex(1)> greeting = "hello" # "hello" # iex(2)> String.upcase(greeting) # "HELLO" # iex(3)> h String.upcase # built-in docs
# $ irb # irb(main):001> greeting = "hello" # => "hello" # irb(main):002> greeting.upcase # => "HELLO" # irb(main):003> greeting.methods.grep(/case/) # => [:casecmp, :upcase, :downcase, ...]
irb is the REPL iex was modeled on. Instead of h for docs, the Ruby move is asking the object itself — greeting.methods, greeting.class, and ri for documentation. Both cells are terminal transcripts, shown display-only.