PONY λ M2 Modula-2

Haskell.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Haskell and Ruby

GHC 9.12 Ruby 4.0
Hello World & Running
Hello, World
main :: IO () main = putStrLn "Hello, World!"
puts "Hello, World!"
Ruby has no main: a script is executed top to bottom, and any statement at the top level runs immediately. There is no IO wrapper — puts is an ordinary method call whose effect happens the moment it is reached.
Side effects anywhere
-- Purity forces effects into the IO type; a pure -- function can never print on its own. greet :: String -> String greet name = "Hi, " ++ name main :: IO () main = putStrLn (greet "Ada")
# Ruby has no purity boundary: any method may print, # read files, or mutate global state at will. def greet(name) puts "(logging)" "Hi, #{name}" end puts greet("Ada")
Ruby draws no line between pure and effectful code. The compiler will not stop you from printing inside a function meant to compute a value, so discipline about side effects is a convention, not a type guarantee.
Comments
main :: IO () main = do -- a line comment {- a block comment -} putStrLn "done"
# a line comment =begin a block comment =end puts "done"
Ruby uses # for line comments and the =begin/=end pair (each at column zero) for block comments. The block form is rarely used in practice; most Rubyists simply prefix each line with #.
print vs puts
main :: IO () main = do print [1, 2, 3] -- uses Show, adds quotes/brackets putStrLn "plain text" -- raw string, no quoting
p [1, 2, 3] # inspect form: [1, 2, 3] puts "plain text" # to_s form, no quotes
Ruby’s p is the analog of print: it renders a value’s inspect (developer-facing, like Show) and returns the argument. puts renders to_s (user-facing) and appends a newline.
Variables & Types
Binding vs assignment
main :: IO () main = do let count = 5 -- immutable binding -- count = 6 -- would not even parse as reassignment print count
count = 5 # a mutable variable count = 6 # reassignment is ordinary and common p count
In Ruby = is assignment to a mutable variable, not a one-shot binding. A name can be reassigned any number of times, and variables are not typed — the same name may hold an integer, then a string.
No type signatures
describe :: Int -> String describe n = "number " ++ show n main :: IO () main = putStrLn (describe 42)
# No signatures, no inference — types are checked # only when an operation actually runs. def describe(value) "value #{value}" end puts describe(42) puts describe("hi") # same method, different type
Ruby is dynamically typed: there are no type signatures and no compile-time type checker. A method accepts whatever it is given, and a type error surfaces only at run time, when an object is sent a message it does not understand.
Everything is an object
-- Values are not objects; functions like 'negate' -- and 'succ' operate on them from the outside. main :: IO () main = do print (negate 5) print (succ 5)
# Even an integer is an object with methods. p 5.abs p 5.succ p 5.class # Integer p 5.methods.size > 100
In Ruby every value is an object, including integers, nil, and true. Operations are methods sent to the receiver (5.succ) rather than free functions applied to a value (succ 5). There are no unboxed primitives.
Runtime type checks
-- The type is known statically; there is nothing -- to test at run time. main :: IO () main = do let value = 42 :: Int print value
value = 42 p value.class # Integer p value.is_a?(Integer) # true p value.is_a?(Numeric) # true — ancestor check p value.respond_to?(:+)
Because types are dynamic, Ruby lets you interrogate an object at run time: class gives its class, is_a? walks the ancestor chain, and respond_to? asks whether it accepts a given message — the duck-typing question that replaces "does it have this instance?"
nil, not Nothing
-- Absence is a value of type Maybe a. lookupName :: Int -> Maybe String lookupName 1 = Just "Ada" lookupName _ = Nothing main :: IO () main = print (lookupName 2)
def lookup_name(id) return "Ada" if id == 1 nil # a single bottom value, not typed absence end p lookup_name(2) # nil p lookup_name(2).nil? # true
Ruby has one universal nil rather than a typed Maybe. Any expression may return nil, and the type system does not force you to handle it, so a forgotten nil becomes a run-time NoMethodError rather than a compile error.
Mutable by default
-- Values are immutable; "changing" a list builds a -- new one and rebinds the name. main :: IO () main = do let numbers = [1, 2, 3] let extended = numbers ++ [4] print extended
numbers = [1, 2, 3] numbers.push(4) # mutates the array in place p numbers # [1, 2, 3, 4] numbers.freeze # opt in to immutability p numbers.frozen? # true
Ruby objects are mutable by default; push changes the array in place rather than returning a new one. Immutability is opt-in via freeze. As of Ruby 4.0, however, string literals are frozen by default — a small step toward Haskell’s default.
Strings
Strings are not [Char]
import Data.Char (toUpper) main :: IO () main = do let greeting = "hello" -- a String IS a list of Char putStrLn (map toUpper greeting)
greeting = "hello" # A String is its own type, not an Array of chars. p greeting.class # String p greeting.upcase # "HELLO" p greeting.chars # ["h","e","l","l","o"]
In Haskell a String is literally [Char], so list functions work on it directly. Ruby’s String is a distinct class with its own rich API; to get a list of characters you ask for chars explicitly.
Interpolation
import Text.Printf (printf) main :: IO () main = do let name = "Ada" let age = 36 :: Int putStrLn (name ++ " is " ++ show age) printf "%s is %d\n" name age
name = "Ada" age = 36 puts "#{name} is #{age}" # any expression works inside #{ } puts "next year: #{age + 1}"
Ruby has built-in string interpolation: #{expression} evaluates any Ruby expression and inserts its to_s. There is no need for show or ++, and no printf-style format string for the common case.
Frozen string literals
main :: IO () main = do -- Immutable, like every Haskell value. let word = "cat" let plural = word ++ "s" putStrLn plural
word = "cat" # frozen in Ruby 4.0 plural = word + "s" # new string, fine puts plural buffer = +"cat" # unary + → mutable copy buffer << "s" # in-place append now allowed puts buffer
Ruby 4.0 freezes string literals by default, so a bare literal is immutable much like a Haskell String. When you genuinely need in-place mutation (<<), opt out with the unary + operator to get a fresh mutable copy.
Common operations
import Data.Char (toUpper) import Data.List (isPrefixOf) main :: IO () main = do let phrase = "hello world" print (length phrase) print (map toUpper phrase) print (words phrase) print ("hello" `isPrefixOf` phrase)
phrase = "hello world" p phrase.length # 11 p phrase.upcase # "HELLO WORLD" p phrase.split # ["hello", "world"] p phrase.start_with?("hello") # true
The operations you reach for Data.List/Data.Char functions for are instance methods on the string itself. split with no argument splits on whitespace, the way words does.
Multiline strings
main :: IO () main = do let poem = "line one\n\ \line two" putStrLn poem
poem = <<~TEXT line one line two TEXT puts poem
Ruby heredocs (<<~TEXT) provide readable multiline literals; the squiggly form strips leading indentation. Haskell has no heredoc, so you rely on explicit \n or the string-gap backslash syntax.
Numbers
One Integer, one Float
main :: IO () main = do let small = 7 :: Int -- machine word let big = 2 ^ 100 :: Integer -- arbitrary precision let ratio = 3.5 :: Double print small print big print ratio
small = 7 # Integer big = 2 ** 100 # still Integer, auto-promoted ratio = 3.5 # Float p small p big p ratio
Ruby has a single Integer class that transparently grows to arbitrary precision — there is no Int/Integer distinction and no overflow. Floating point is Float (a double). You never annotate which one you mean.
Division & modulo
main :: IO () main = do print (div 7 2) -- 3, integer division print (mod 7 2) -- 1 print (7 / 2 :: Double) -- 3.5, needs Fractional
p 7 / 2 # 3 — integer / integer stays Integer p 7 % 2 # 1 p 7.fdiv(2) # 3.5 — explicit float division p 7.0 / 2 # 3.5 — one Float infects the result
Ruby overloads /: integer divided by integer truncates like div, but if either operand is a Float the result is a Float. Where Haskell separates div from / by type class, Ruby decides by the runtime classes of the operands.
Methods on numbers
main :: IO () main = do print (abs (-4)) print (even 10) print (gcd 12 18)
p(-4.abs) # 4 p 10.even? # true p 12.gcd(18) # 6 p 3.times.to_a # [0, 1, 2]
Numbers are objects, so arithmetic helpers are methods: 10.even?, 12.gcd(18). Predicates end in ? by convention. 3.times is a common idiom that turns an integer into an iterator.
Summing a range
main :: IO () main = do print (sum [1 .. 100]) print (product [1 .. 5])
p (1..100).sum # 5050 p (1..5).reduce(:*) # 120 p (1..5).inject(:*) # 120 — inject is an alias
A Ruby Range mixes in Enumerable, so it answers sum, reduce, map, and the rest directly. reduce(:*) passes the multiplication operator as a symbol, the terse equivalent of product.
Lists → Arrays
List → Array
main :: IO () main = do let numbers = [1, 2, 3] let more = 0 : numbers -- cons print more
numbers = [1, 2, 3] more = [0] + numbers # concatenation, new array p more # [0, 1, 2, 3] # Arrays are heterogeneous, unlike Haskell lists. mixed = [1, "two", :three, 4.0] p mixed
A Ruby Array is a growable, mutable, heterogeneous sequence — closer to a vector than a cons list. There is no cheap head-cons; prepending builds a new array. And because Ruby is dynamic, one array may hold values of different types.
head, tail, indexing
main :: IO () main = do let items = [10, 20, 30, 40] print (head items) print (tail items) print (items !! 2) print (last items)
items = [10, 20, 30, 40] p items.first # 10 p items[1..] # [20, 30, 40] p items[2] # 30 — O(1) random access p items.last # 40 p items[-1] # 40 — negative indices count from the end
Arrays support O(1) indexing, including negative indices from the end. first on an empty array returns nil rather than throwing the way head does on [], so the "partial function" trap is a quiet nil instead of an exception.
List comprehensions
main :: IO () main = do let squares = [x * x | x <- [1 .. 5], even x] print squares
squares = (1..5).select(&:even?).map { |x| x * x } p squares # [4, 16] # or in one filter_map pass: squares2 = (1..5).filter_map { |x| x * x if x.even? } p squares2 # [4, 16]
Ruby has no comprehension syntax; the idiom is a chain of Enumerable methods. select is the guard, map is the output expression, and filter_map fuses the two into a single pass.
In-place mutation
-- No mutation: each step yields a new list. main :: IO () main = do let stack = [1, 2, 3] let pushed = stack ++ [4] let (top, rest) = (last pushed, init pushed) print top print rest
stack = [1, 2, 3] stack.push(4) # in place → [1, 2, 3, 4] top = stack.pop # in place → 4, array now [1, 2, 3] p top p stack
Ruby arrays are mutable stacks and queues out of the box: push/pop/shift/unshift all change the receiver. This is the everyday style in Ruby, whereas in Haskell you would thread a new list through each step.
sort, reverse, uniq
import Data.List (sort, nub) main :: IO () main = do let values = [3, 1, 2, 3, 1] print (sort values) print (reverse values) print (nub values)
values = [3, 1, 2, 3, 1] p values.sort # [1, 1, 2, 3, 3] p values.reverse # [1, 3, 2, 1, 3] p values.uniq # [3, 1, 2] p values.sort! # sort! mutates in place
The bang suffix (sort!) marks the mutating variant that sorts the array in place and returns it; the plain sort returns a new array. Ruby’s uniq corresponds to nub but is backed by a hash, so it is far cheaper.
Tuples & Destructuring
Tuples
main :: IO () main = do let point = (3, 4) -- fixed arity, mixed types print (fst point) print (snd point)
point = [3, 4] # Ruby uses arrays for tuples p point[0] # 3 p point[1] # 4 # fixed-size value type when you want it: Point = Data.define(:x, :y) p Point.new(3, 4).x
Ruby has no distinct tuple type; a short array plays that role, with none of Haskell’s per-position type guarantees. When you want a real fixed-shape value with named fields, Data.define (Ruby 3.2+) is the closest analog to a tuple or small record.
Destructuring bind
main :: IO () main = do let (name, age) = ("Ada", 36) putStrLn name print age
name, age = "Ada", 36 # parallel assignment puts name p age first, *rest = [1, 2, 3, 4] # splat captures the tail p first # 1 p rest # [2, 3, 4]
Ruby’s parallel assignment destructures the right-hand side positionally, and the splat * captures "the rest" the way a cons pattern (first : rest) does — but it is a statement form, not a pattern in a function head.
swap and zip
main :: IO () main = do let (a, b) = (1, 2) print (b, a) -- swapped print (zip [1, 2, 3] "abc")
a, b = 1, 2 a, b = b, a # swap without a temporary p [a, b] # [2, 1] p [1, 2, 3].zip(["a", "b", "c"]) # [[1, "a"], [2, "b"], [3, "c"]]
Because the right side of a parallel assignment is fully evaluated before binding, a, b = b, a swaps without a temporary. zip pairs element-wise like Haskell’s, but yields arrays of arrays rather than a list of tuples.
Maps → Hashes
Data.Map → Hash
import qualified Data.Map as Map main :: IO () main = do let ages = Map.fromList [("Ada", 36), ("Alan", 41)] print (Map.lookup "Ada" ages) print (Map.size ages)
ages = { "Ada" => 36, "Alan" => 41 } p ages["Ada"] # 36 p ages.size # 2 # symbol keys get their own shorthand: config = { host: "localhost", port: 8080 } p config[:port] # 8080
A Ruby Hash is a built-in literal, not a library import, and it preserves insertion order. Keys are commonly Symbols (:port) — interned, immutable identifiers with the host: shorthand — which have no direct Haskell equivalent.
Lookup returns nil
import qualified Data.Map as Map import Data.Maybe (fromMaybe) main :: IO () main = do let ages = Map.fromList [("Ada", 36)] -- lookup is total: it returns Maybe print (fromMaybe 0 (Map.lookup "Zed" ages))
ages = { "Ada" => 36 } p ages["Zed"] # nil — missing key p ages.fetch("Ada") # 36 p ages.fetch("Zed", 0) # 0 — default p ages.fetch("Zed") rescue "would raise KeyError"
Indexing a Hash with a missing key returns nil rather than a typed Maybe. When you want the total, explicit behavior of Map.lookup, use fetch: it takes a default or raises KeyError instead of silently yielding nil.
Insert & update
import qualified Data.Map as Map main :: IO () main = do let start = Map.fromList [("a", 1)] let updated = Map.insert "b" 2 start -- new map print (Map.toList updated)
counts = { "a" => 1 } counts["b"] = 2 # mutates the hash in place p counts # {"a"=>1, "b"=>2} # frequency count idiom with a default block: tally = Hash.new(0) "banana".each_char { |letter| tally[letter] += 1 } p tally # {"b"=>1, "a"=>3, "n"=>2}
Assignment to a hash key mutates the hash rather than returning a new one. Hash.new(0) supplies a default for absent keys, giving the classic one-line frequency count — a pattern that in Haskell needs insertWith or a fold.
Iterating a map
import qualified Data.Map as Map main :: IO () main = do let ages = Map.fromList [("Ada", 36), ("Alan", 41)] mapM_ (\(name, age) -> putStrLn (name ++ ": " ++ show age)) (Map.toList ages)
ages = { "Ada" => 36, "Alan" => 41 } ages.each do |name, age| puts "#{name}: #{age}" end p ages.keys # ["Ada", "Alan"] p ages.map { |name, age| age }.sum # 77
Iterating a Hash yields each key–value pair, which the block destructures into two parameters. Hashes are Enumerable, so map, select, keys, and values all work directly, the way Map.toList plus a fold would in Haskell.
Ranges & Lazy Evaluation
Eager by default
main :: IO () main = do -- Lazy: only the first five are ever computed. let squares = map (^ 2) [1 ..] print (take 5 squares)
# Eager: (1..10).map runs immediately and fully. squares = (1..10).map { |n| n ** 2 } p squares.first(5) # [1, 4, 9, 16, 25] # (1..) is an endless range; mapping it eagerly # would loop forever — see the lazy example next.
Ruby evaluates eagerly: map on a finite range computes every element right away. There is no pervasive laziness, so an infinite range fed to an eager map hangs. Laziness in Ruby is a deliberate, opt-in mode rather than the default.
Opt-in laziness
main :: IO () main = do let evens = filter even [1 ..] print (take 5 evens)
evens = (1..Float::INFINITY).lazy.select(&:even?) p evens.first(5) # [2, 4, 6, 8, 10] # take is lazy too; force with .to_a or .first firstThree = (1..).lazy.map { |n| n * n }.first(3) p firstThree # [1, 4, 9]
Inserting .lazy turns an Enumerator into a lazy pipeline that pulls elements on demand, so it can front an infinite range exactly like Haskell’s default evaluation — but you must remember to force it with first, take, or to_a.
Ranges
import Data.Char (chr, ord) main :: IO () main = do print [1 .. 5] print [1, 3 .. 9] -- step of 2 print ['a' .. 'e']
p (1..5).to_a # [1, 2, 3, 4, 5] p (1...5).to_a # [1, 2, 3, 4] — end-exclusive p (1..9).step(2).to_a # [1, 3, 5, 7, 9] p ("a".."e").to_a # ["a", "b", "c", "d", "e"]
Ruby distinguishes inclusive (..) from exclusive (...) ranges, and a stepped range uses step rather than Haskell’s "first, second .." arithmetic notation. Ranges are lazy sequences until you force them with to_a.
Functions & Methods
Defining a function
add :: Int -> Int -> Int add x y = x + y main :: IO () main = print (add 3 4)
def add(x, y) x + y # last expression is the return value end p add(3, 4) # 7
A Ruby method is introduced with def and returns its last evaluated expression implicitly, so an explicit return is rarely written. Parameters are listed in parentheses, and there is no signature line above the definition.
One-line definitions
double :: Int -> Int double x = x * 2 main :: IO () main = print (double 21)
def double(x) = x * 2 # endless method (Ruby 3.0+) p double(21) # 42
Ruby’s endless method definition, def name(args) = expression, mirrors the compact equation form double x = x * 2 for one-expression methods. It is a modern convenience; the multi-line def/end form remains the norm for anything larger.
Default arguments
-- No default arguments; supply a wrapper. greet :: String -> String greet name = "Hello, " ++ name greetWorld :: String greetWorld = greet "World" main :: IO () main = putStrLn greetWorld
def greet(name = "World") "Hello, #{name}" end puts greet # "Hello, World" puts greet("Ada") # "Hello, Ada"
Ruby supports default parameter values directly in the signature, and a default may even reference earlier parameters. Haskell has no defaults, so the usual workaround is an extra wrapper function or a record of options.
Keyword arguments
-- Emulated with a record of named fields. data Options = Options { width :: Int, height :: Int } area :: Options -> Int area opts = width opts * height opts main :: IO () main = print (area (Options { width = 3, height = 4 }))
def area(width:, height:) width * height end # order-independent, self-documenting call site: p area(height: 4, width: 3) # 12
Ruby keyword arguments (the trailing colon in width:) are passed by name in any order and are effectively required unless given a default. They give call sites the self-documenting quality that Haskell reaches for with record syntax.
Variadic arguments
-- Fixed arity; a list stands in for "many". total :: [Int] -> Int total = sum main :: IO () main = print (total [1, 2, 3, 4])
def total(*numbers) # splat collects all positionals numbers.sum end p total(1, 2, 3, 4) # 10 p total # 0
The splat parameter *numbers gathers any number of positional arguments into an array, giving true variadic methods. Haskell reaches the same expressiveness by taking a single list argument, since every function has fixed arity.
Guards → if/case
classify :: Int -> String classify n | n < 0 = "negative" | n == 0 = "zero" | otherwise = "positive" main :: IO () main = putStrLn (classify (-5))
def classify(n) if n < 0 "negative" elsif n.zero? "zero" else "positive" end end puts classify(-5) # "negative"
Ruby has no guard syntax on method definitions; the equivalent is an if/elsif/else expression, which — like everything in Ruby — returns a value. There is no otherwise; the final else plays that role.
Higher-Order Functions & Blocks
map, filter, fold
main :: IO () main = do let numbers = [1, 2, 3, 4, 5] print (map (* 2) numbers) print (filter even numbers) print (foldl (+) 0 numbers)
numbers = [1, 2, 3, 4, 5] p numbers.map { |n| n * 2 } # [2, 4, 6, 8, 10] p numbers.select(&:even?) # [2, 4] p numbers.reduce(0) { |acc, n| acc + n } # 15
The core higher-order functions are Enumerable methods that take a block rather than a function argument. filter is select, and foldl is reduce (also aliased inject). A block is Ruby’s lightweight anonymous function.
Blocks & yield
-- A function that takes another function. applyTwice :: (a -> a) -> a -> a applyTwice f x = f (f x) main :: IO () main = print (applyTwice (+ 3) 10)
def apply_twice yield(yield(10)) # calls the passed-in block end p apply_twice { |x| x + 3 } # 16
Every Ruby method can receive one anonymous block, invoked with yield, without declaring it as a parameter. This block-passing convention is Ruby’s idiom for higher-order behavior, distinct from passing a first-class function value as an ordinary argument.
Lambdas & procs
main :: IO () main = do let increment = \x -> x + 1 print (increment 41) print (map (\x -> x * x) [1, 2, 3])
increment = ->(x) { x + 1 } # a lambda p increment.call(41) # 42 p increment.(41) # 42 — shorthand p [1, 2, 3].map { |x| x * x } # [1, 4, 9]
The stabby lambda ->(x) { ... } is Ruby’s first-class function value, corresponding to \x -> .... Unlike a bare block, a lambda is a real object you can store in a variable and must invoke explicitly with call (or the .() shorthand).
Currying & partial application
-- Currying is automatic; every function is unary. add :: Int -> Int -> Int add x y = x + y main :: IO () main = do let addFive = add 5 -- partial application print (addFive 10)
add = ->(x, y) { x + y } add_five = add.curry[5] # opt-in currying p add_five[10] # 15 # partial application via curry: p [1, 2, 3].map(&add_five) # [6, 7, 8]
Ruby methods are not curried by default: add(5) with a missing argument raises an ArgumentError. To get Haskell-style partial application you call curry on a Proc, which returns a chain that accepts arguments one at a time.
Function composition
main :: IO () main = do let process = show . (* 2) . (+ 1) putStrLn (process 20) -- (20+1)*2 = 42
increment = ->(x) { x + 1 } double = ->(x) { x * 2 } process = increment >> double # left-to-right p process.call(20) # 42 compose = double << increment # right-to-left, like . p compose.call(20) # 42
Ruby composes Procs with >> (left-to-right) and << (right-to-left, matching Haskell’s .). In everyday code, though, method chaining on the data (value.step_one.step_two) is far more common than point-free composition of functions.
Method references
import Data.Char (toUpper) main :: IO () main = do -- Point-free: pass the function by name. print (map toUpper "hello")
p ["hello", "world"].map(&:upcase) # ["HELLO", "WORLD"] # &:upcase means ->(s) { s.upcase } p [1, -2, 3].map(&:abs) # [1, 2, 3]
The &:method_name idiom converts a symbol into a block that calls that method on each element — Ruby’s terse stand-in for passing a named function point-free. It works because & asks the symbol for its to_proc.
Pattern Matching
case … of → case/in
describe :: Int -> String describe n = case n of 0 -> "zero" 1 -> "one" _ -> "many" main :: IO () main = putStrLn (describe 1)
def describe(n) case n in 0 then "zero" in 1 then "one" else "many" end end puts describe(1) # "one"
Ruby’s case/in (pattern matching, since Ruby 3.0) is the closest analog to case … of. Note the separate, older case/when form uses === equality instead of structural matching — in is the one that deconstructs.
Deconstructing lists
sumPair :: [Int] -> String sumPair [a, b] = "two: " ++ show (a + b) sumPair (x : _) = "head: " ++ show x sumPair [] = "empty" main :: IO () main = putStrLn (sumPair [3, 4])
def sum_pair(list) case list in [a, b] then "two: #{a + b}" in [head, *] then "head: #{head}" in [] then "empty" end end puts sum_pair([3, 4]) # "two: 7"
Array patterns in case/in deconstruct positionally, and * matches "the rest" like a cons tail. Unlike Haskell, these patterns live only inside case/in, not in the method head — there is no clause-per-equation definition style.
Pattern guards & binding
check :: (Int, Int) -> String check (x, y) | x == y = "equal" | otherwise = "differ by " ++ show (abs (x - y)) main :: IO () main = putStrLn (check (3, 7))
def check(pair) case pair in [x, y] if x == y then "equal" in [x, y] then "differ by #{(x - y).abs}" end end puts check([3, 7]) # "differ by 4"
A pattern can carry a guard with a trailing if, and the bound names (x, y) are in scope for both the guard and the body — the same shape as Haskell’s pattern guards, expressed inside case/in.
Matching hashes
-- Deconstruct a record by its fields. data User = User { name :: String, role :: String } describe :: User -> String describe (User { name = n, role = "admin" }) = n ++ " is an admin" describe (User { name = n }) = n ++ " is a user" main :: IO () main = putStrLn (describe (User "Ada" "admin"))
def describe(user) case user in { name:, role: "admin" } then "#{name} is an admin" in { name: } then "#{name} is a user" end end puts describe({ name: "Ada", role: "admin" })
Hash patterns match by key and bind the value to a same-named local: { name: } binds name. Pinning a literal value (role: "admin") constrains the match, giving the field-level deconstruction that record patterns provide in Haskell.
Algebraic Data Types → Classes
Records → Data/Struct
data Point = Point { x :: Int, y :: Int } deriving Show main :: IO () main = do let origin = Point { x = 0, y = 0 } print (x origin) print origin
Point = Data.define(:x, :y) # immutable value object origin = Point.new(x: 0, y: 0) p origin.x # 0 p origin # #<data Point x=0, y=0> moved = origin.with(x: 5) # copy with one field changed p moved
Data.define (Ruby 3.2+) creates an immutable value class with reader methods and structural equality — the closest match to a deriving (Show, Eq) record. Its with method performs the functional-update copy that Haskell record-update syntax gives you.
Sum types → subclasses
data Shape = Circle Double | Rectangle Double Double area :: Shape -> Double area (Circle r) = pi * r * r area (Rectangle w h) = w * h main :: IO () main = do print (area (Circle 2)) print (area (Rectangle 3 4))
Circle = Data.define(:radius) Rectangle = Data.define(:width, :height) def area(shape) case shape in Circle(radius:) then Math::PI * radius * radius in Rectangle(width:, height:) then width * height end end p area(Circle.new(radius: 2)) p area(Rectangle.new(width: 3, height: 4))
Ruby has no built-in sum type; you model the variants as separate classes and dispatch over them with case/in. This works, but nothing checks exhaustiveness — forget a variant and the case silently returns nil instead of failing to compile.
Classes with behavior
-- Data and functions are separate. data Counter = Counter { count :: Int } increment :: Counter -> Counter increment (Counter n) = Counter (n + 1) main :: IO () main = print (count (increment (Counter 0)))
class Counter def initialize(count = 0) @count = count # instance variable, mutable end def increment @count += 1 # mutates self self end attr_reader :count end counter = Counter.new counter.increment.increment p counter.count # 2
A Ruby class bundles mutable state (@count) with the methods that act on it — the object-oriented inverse of Haskell’s separation of data from functions. attr_reader generates the getter, and returning self enables the fluent chaining seen here.
newtype → wrapper class
newtype Email = Email String address :: Email -> String address (Email s) = s main :: IO () main = putStrLn (address (Email "a@b.com"))
class Email def initialize(address) raise ArgumentError, "invalid" unless address.include?("@") @address = address end attr_reader :address end p Email.new("a@b.com").address
Ruby has no zero-cost newtype; a wrapper is a full object with its own allocation. The upside is that the constructor can enforce invariants at build time (here, that the string contains an @) — validation that a Haskell newtype would push into a smart constructor.
Typeclasses → Modules
Typeclasses → duck typing
class Describable a where describe :: a -> String instance Describable Bool where describe True = "yes" describe False = "no" main :: IO () main = putStrLn (describe True)
# No declared interface — any object that responds # to #describe qualifies. def announce(thing) puts thing.describe end class Dog def describe = "a dog" end announce(Dog.new) # "a dog"
Ruby replaces typeclass constraints with duck typing: a method works on any object that responds to the messages it sends, with no instance declaration and no compile-time check. "If it quacks like a duck" is the entire contract.
Ord → Comparable
data Version = Version Int Int deriving (Eq) instance Ord Version where compare (Version a b) (Version c d) = compare (a, b) (c, d) main :: IO () main = print (Version 1 2 < Version 1 5)
class Version include Comparable # mixin grants <, >, ==, between? attr_reader :major, :minor def initialize(major, minor) @major, @minor = major, minor end def <=>(other) # define one operator... [major, minor] <=> [other.major, other.minor] end end p Version.new(1, 2) < Version.new(1, 5) # true
Including the Comparable module and defining the single <=> (spaceship) operator gives you <, <=, ==, >, and between? for free — the mixin analog of an Ord instance derived from one compare.
Show → to_s / inspect
data Color = Red | Green | Blue instance Show Color where show Red = "#f00" show Green = "#0f0" show Blue = "#00f" main :: IO () main = putStrLn (show Green)
class Color def initialize(name) = @name = name def to_s = "##{ { "red" => "f00", "green" => "0f0" }[@name] }" def inspect = "#<Color #{to_s}>" end puts Color.new("green") # to_s → "#0f0" p Color.new("green") # inspect → #<Color #0f0>
Defining to_s customizes the user-facing rendering (what puts and interpolation use), while inspect customizes the developer-facing form (what p uses) — Ruby’s split of the single Show class into two conventional methods.
Foldable → Enumerable
-- Foldable gives sum, toList, length, etc. data Tree a = Leaf a | Node (Tree a) (Tree a) toList :: Tree a -> [a] toList (Leaf value) = [value] toList (Node left right) = toList left ++ toList right main :: IO () main = print (sum (toList (Node (Leaf 1) (Leaf 2))))
class NumberBag include Enumerable # one method → the whole toolbox def initialize(*values) = @values = values def each(&block) = @values.each(&block) end bag = NumberBag.new(1, 2, 3) p bag.sum # 6 p bag.map { |n| n * 10 } # [10, 20, 30] p bag.max # 3
Including Enumerable and defining a single each yields map, select, sum, max, reduce, and dozens more — the mixin counterpart to Foldable, where implementing foldr earns the whole derived API.
Maybe/Either → nil & Exceptions
Maybe → nil & &.
import Data.Maybe (fromMaybe) import Text.Read (readMaybe) parseAge :: String -> Int parseAge text = fromMaybe (-1) (readMaybe text) main :: IO () main = do print (parseAge "42") print (parseAge "oops")
def parse_age(text) Integer(text) # raises on bad input rescue ArgumentError nil end p parse_age("42") # 42 p parse_age("oops") # nil p parse_age("oops")&.abs # nil — safe navigation short-circuits
The safe-navigation operator &. is Ruby’s answer to chaining over Maybe: value&.method returns nil instead of calling the method when value is nil, much like fmap over a Nothing.
Default values
import Data.Maybe (fromMaybe) import qualified Data.Map as Map main :: IO () main = do let config = Map.fromList [("port", "8080")] putStrLn (fromMaybe "80" (Map.lookup "host" config))
config = { "port" => "8080" } port = config["host"] || "80" # nil is falsy puts port # "80" # but beware: false and nil are both falsy flag = config["debug"] || true # loses an explicit false
The || operator supplies a fallback the way fromMaybe does, because nil is falsy. The catch Haskell never has: false is also falsy, so || cannot distinguish "absent" from "present and false" — use fetch when that matters.
Either → exceptions
safeDiv :: Int -> Int -> Either String Int safeDiv _ 0 = Left "divide by zero" safeDiv a b = Right (a `div` b) main :: IO () main = case safeDiv 10 0 of Left err -> putStrLn ("error: " ++ err) Right n -> print n
def safe_div(a, b) raise ZeroDivisionError, "divide by zero" if b.zero? a / b end begin puts safe_div(10, 0) rescue ZeroDivisionError => error puts "error: #{error.message}" end
Ruby models recoverable failure with exceptions and begin/rescue rather than an Either return, so failure is an out-of-band control-flow effect, not a value in the type. Nothing forces the caller to handle it — an unrescued exception unwinds the stack.
bracket → ensure
import Control.Exception (bracket_) main :: IO () main = bracket_ (putStrLn "acquire") (putStrLn "release") -- always runs (putStrLn "use")
def with_resource puts "acquire" yield ensure puts "release" # runs on normal exit AND on exception end with_resource { puts "use" }
The ensure clause runs whether the body returns normally or raises, giving the guaranteed-cleanup behavior of bracket/finally. The block-plus-ensure method is the idiomatic Ruby way to lend a resource and reclaim it.
IO & Files
do-notation → statements
main :: IO () main = do putStrLn "one" putStrLn "two" let total = 1 + 2 print total
puts "one" puts "two" total = 1 + 2 # no monad; just a sequence of statements p total
Ruby has no IO monad and no do notation because effects are unrestricted: a script is simply a sequence of statements executed in order. What do desugars to sequencing, Ruby gets for free from ordinary evaluation order.
mapM_ → each
main :: IO () main = do mapM_ print [1, 2, 3] mapM_ putStrLn ["a", "b"]
[1, 2, 3].each { |n| p n } ["a", "b"].each { |letter| puts letter } # each returns the receiver, not a wrapped unit: result = [1, 2].each { |n| n } p result # [1, 2]
Iterating for side effects is just each with a block; there is no mapM_/forM_ distinction because there is no monadic context to thread. each returns the collection itself rather than a unit value, which is why it chains.
Reading input
Standard input is unavailable in the in-browser runtime, so this example is shown for reference only.
main :: IO () main = do putStrLn "What is your name?" name <- getLine putStrLn ("Hello, " ++ name)
puts "What is your name?" name = gets.chomp # reads a line, strips the newline puts "Hello, #{name}"
gets reads one line from standard input as a string (including the trailing newline, which chomp removes) — the eager, untyped counterpart to getLine. It returns nil at end of input rather than raising.
Reading a file
File I/O is unavailable in the in-browser runtime, so this example is shown for reference only.
main :: IO () main = do contents <- readFile "notes.txt" mapM_ putStrLn (lines contents)
contents = File.read("notes.txt") contents.each_line do |line| puts line.chomp end # whole file as an array of lines: lines = File.readlines("notes.txt", chomp: true)
File.read slurps the whole file into a string eagerly; readlines returns an array of lines. Because Ruby is impure and eager, there is no lazy stream and no IO wrapper around the result — you get the string immediately.
Formatted output
import Text.Printf (printf) main :: IO () main = do printf "%-6s %5.2f\n" "total" (3.14159 :: Double) printf "%03d\n" (7 :: Int)
printf("%-6s %5.2f\n", "total", 3.14159) puts format("%03d", 7) # "007" puts "%05.2f" % 3.14159 # "03.14" via % operator
Ruby keeps C-style format strings via printf, the format method, and the % operator on a string — the same conversion specifiers you know from Text.Printf, so this is one corner where the two languages look nearly identical.