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.