PONY λ M2 Modula-2

Ruby.CodeCompared.To/OCaml

An interactive executable cheatsheet for Rubyists learning OCaml

Ruby 4.0 OCaml 5.2
Output & Basics
Hello World
puts "Hello, World!"
let () = print_endline "Hello, World!"

print_endline prints a string followed by a newline — the direct equivalent of Ruby's puts. The let () = ... pattern is OCaml's way of executing a side-effecting expression at the top level; the unit value () discards the result.

Multiple outputs
puts "First" puts "Second" puts "Third"
let () = print_endline "First"; print_endline "Second"; print_endline "Third"

The semicolon ; sequences expressions in OCaml: it evaluates the left side (which must return unit), discards the result, and continues with the right side. This is the OCaml equivalent of Ruby's newline-separated statements.

print_string vs print_endline
print "Hello" print ", " puts "World!"
let () = print_string "Hello"; print_string ", "; print_endline "World!"

print_string writes without a trailing newline, like Ruby's print. print_endline appends a newline, like Ruby's puts. OCaml also provides print_newline () to emit a newline on its own.

Formatted output
printf "%s is %d years old ", "Alice", 30
let () = Printf.printf "%s is %d years old " "Alice" 30

Printf.printf works like C's printf — format string followed by arguments. Unlike Ruby's comma-separated arguments, OCaml uses curried function application: each argument is space-separated. The format string is type-checked at compile time.

Printing numbers
puts 42 puts 3.14
let () = Printf.printf "%d " 42; Printf.printf "%g " 3.14

OCaml has separate functions for printing each type: print_int, print_float, print_string, print_bool. Using Printf.printf with format specifiers is more flexible. The %g specifier removes trailing zeros from floats.

Values & Bindings
Immutable binding
number = 42 puts number
let number = 42 let () = Printf.printf "%d " number

In OCaml, let creates an immutable binding — it cannot be reassigned. This is different from Ruby's variables, which can be freely reassigned. Attempting to write number = 99 after the binding is a compile error.

Mutable reference
counter = 0 counter += 1 puts counter
let counter = ref 0 let () = counter := !counter + 1; Printf.printf "%d " !counter

Mutation in OCaml is explicit: ref allocates a mutable cell, ! dereferences it (reads the value), and := assigns a new value. This contrasts with Ruby, where all variables are mutable by default.

Local bindings with let...in
result = begin greeting = "Hello" name = "Alice" "#{greeting}, #{name}!" end puts result
let () = let result = let greeting = "Hello" in let name = "Alice" in greeting ^ ", " ^ name ^ "!" in print_endline result

let ... in ... creates a locally scoped binding that only exists within the in expression. Nested let...in blocks read top-to-bottom, similar to a Ruby begin...end block, but each name is truly local.

Let shadowing
number = 1 number = number + 1 puts number
let () = let number = 1 in let number = number + 1 in Printf.printf "%d " number

A let binding can shadow an earlier one with the same name. This looks like reassignment but is not — the original binding still exists; the new one simply takes precedence in the inner scope. The right-hand side of the second let still refers to the original value.

Type inference
# Ruby infers types at runtime number = 42 greeting = "hello" active = true puts "#{number} #{greeting} #{active}"
(* OCaml infers types at compile time *) let number = 42 let greeting = "hello" let active = true let () = Printf.printf "%d %s %b " number greeting active

OCaml uses Hindley-Milner type inference: every value has a precise static type, but you rarely need to write it. The compiler deduces int, string, and bool automatically. The %b format specifier prints booleans as true or false.

The unit type
def say_hello puts "Hello" end say_hello
let say_hello () = print_endline "Hello" let () = say_hello ()

OCaml's unit type has exactly one value: (). It plays the role Ruby's nil plays for side-effecting methods — it signals "this returns no meaningful value." Functions that take no arguments accept (), and let () = ... at the top level executes a side effect and discards the result.

Types
Numeric types
integer_value = 42 float_value = 3.14 puts integer_value + 1 puts float_value + 0.01
let integer_value = 42 let float_value = 3.14 let () = Printf.printf "%d " (integer_value + 1); Printf.printf "%g " (float_value +. 0.01)

OCaml has separate arithmetic operators for integers and floats: + - * / for int, and +. -. *. /. for float. Mixing them without explicit conversion is a compile error — unlike Ruby, where numeric types interoperate freely.

Type conversion
puts 3.7.to_i puts 42.to_f puts 42.to_s puts Integer("42")
let () = Printf.printf "%d " (int_of_float 3.7); Printf.printf "%g " (float_of_int 42); print_endline (string_of_int 42); Printf.printf "%d " (int_of_string "42")

OCaml uses explicit conversion functions: int_of_float truncates (no rounding), float_of_int, string_of_int, int_of_string. Ruby's .to_i and .to_f methods are instance methods on the value; OCaml's conversions are plain functions.

Boolean operations
puts true && false puts true || false puts !true puts (1 == 1) puts (1 != 2)
let () = Printf.printf "%b " (true && false); Printf.printf "%b " (true || false); Printf.printf "%b " (not true); Printf.printf "%b " (1 = 1); Printf.printf "%b " (1 <> 2)

In OCaml, = is structural equality (like Ruby's ==) and <> is inequality (like !=). Boolean negation is the function not rather than a ! operator. Physical (reference) equality uses ==.

Characters
letter = 'a' puts letter puts letter.ord puts letter.upcase
let letter = 'a' let () = Printf.printf "%c " letter; Printf.printf "%d " (Char.code letter); Printf.printf "%c " (Char.uppercase_ascii letter)

OCaml has a distinct char type for single characters, written with single quotes. Char.code returns the ASCII code (equivalent to Ruby's .ord), and Char.chr converts back. Characters and single-character strings are different types.

Type annotations
# Ruby: Sorbet/RBS for static types # sig { params(number: Integer).returns(Integer) } def double(number) = number * 2 puts double(21)
let double (number : int) : int = number * 2 let () = Printf.printf "%d " (double 21)

Type annotations in OCaml are written with : type after the name. They are optional — the compiler infers them — but useful for documentation and to trigger clearer error messages. The return type annotation goes after the last parameter, before =.

Strings
String concatenation
greeting = "Hello" + ", " + "World!" puts greeting
let greeting = "Hello" ^ ", " ^ "World!" let () = print_endline greeting

OCaml uses the ^ operator for string concatenation. Ruby uses +. The ^ operator creates a new string — OCaml strings are immutable by default (though technically they are byte arrays that can be mutated with Bytes).

String length and case
message = "Hello, World!" puts message.length puts message.upcase puts message.downcase
let message = "Hello, World!" let () = Printf.printf "%d " (String.length message); print_endline (String.uppercase_ascii message); print_endline (String.lowercase_ascii message)

String.length returns the byte length (not character count for multi-byte encodings). The _ascii suffix on String.uppercase_ascii and String.lowercase_ascii indicates that only ASCII letters are transformed — full Unicode case conversion requires a third-party library.

Trim and substring
message = " Hello, World! " puts message.strip puts message.strip[0, 5]
let message = " Hello, World! " let () = let trimmed = String.trim message in print_endline trimmed; print_endline (String.sub trimmed 0 5)

String.trim removes leading and trailing whitespace. String.sub takes a string, a start index, and a length — equivalent to Ruby's String#[] with two arguments. Attempting to access out-of-bounds bytes raises Invalid_argument.

String interpolation / sprintf
name = "Alice" age = 30 sentence = "#{name} is #{age} years old" puts sentence
let name = "Alice" let age = 30 let sentence = Printf.sprintf "%s is %d years old" name age let () = print_endline sentence

OCaml has no string interpolation syntax. Instead, Printf.sprintf builds a formatted string and returns it — analogous to Ruby's format or sprintf. The format string is type-checked at compile time, so passing the wrong type is a compile error, not a runtime error.

Splitting strings
csv = "one,two,three" parts = csv.split(",") parts.each { |part| puts part }
let csv = "one,two,three" let () = let parts = String.split_on_char ',' csv in List.iter print_endline parts

String.split_on_char splits on a single char, returning a string list. Ruby's split accepts a string or regex; OCaml's stdlib split accepts only a single character. For regex-based splitting, the Re or Str libraries are needed.

Collections
List literal
numbers = [1, 2, 3, 4, 5] puts numbers.length numbers.each { |number| print "#{number} " } puts
let numbers = [1; 2; 3; 4; 5] let () = Printf.printf "%d " (List.length numbers); List.iter (fun number -> Printf.printf "%d " number) numbers; print_newline ()

OCaml lists use semicolons as separators inside square brackets — not commas. A comma creates a tuple, so [1, 2] is a one-element list containing the pair (1, 2). Lists are immutable singly-linked lists; all elements must share the same type.

Cons operator
numbers = [2, 3, 4] all_numbers = [1] + numbers all_numbers.each { |number| print "#{number} " } puts
let numbers = [2; 3; 4] let all_numbers = 1 :: numbers let () = List.iter (fun number -> Printf.printf "%d " number) all_numbers; print_newline ()

The :: cons operator prepends an element to a list in O(1). It is the same operator used in pattern matching (head :: tail). OCaml lists are immutable singly-linked lists, so prepending is efficient, but appending (@) requires copying the left list.

List.map
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |number| number * 2 } doubled.each { |number| puts number }
let numbers = [1; 2; 3; 4; 5] let () = let doubled = List.map (fun number -> number * 2) numbers in List.iter (fun number -> Printf.printf "%d " number) doubled

List.map applies a function to every element and returns a new list — identical in behavior to Ruby's Enumerable#map. In OCaml, the function comes first and the list second; in Ruby, the list is the receiver and the block is the argument.

List.filter
numbers = [1, 2, 3, 4, 5, 6] evens = numbers.select { |number| number % 2 == 0 } evens.each { |number| puts number }
let numbers = [1; 2; 3; 4; 5; 6] let () = let evens = List.filter (fun number -> number mod 2 = 0) numbers in List.iter (fun number -> Printf.printf "%d " number) evens

List.filter is the OCaml equivalent of Ruby's select/filter. Note that OCaml uses mod for the remainder operator (Ruby uses %), and = for equality comparison (Ruby uses ==).

List.fold_left
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |accumulator, number| accumulator + number } puts total
let numbers = [1; 2; 3; 4; 5] let () = let total = List.fold_left (fun accumulator number -> accumulator + number) 0 numbers in Printf.printf "%d " total

List.fold_left is OCaml's left fold — equivalent to Ruby's reduce or inject. The accumulator comes first in the function arguments. There is also List.fold_right, which processes from right to left (and has reversed argument order).

Arrays
items = ["apple", "banana", "cherry"] items[1] = "blueberry" puts items[1] puts items.length
let items = [|"apple"; "banana"; "cherry"|] let () = items.(1) <- "blueberry"; print_endline items.(1); Printf.printf "%d " (Array.length items)

OCaml arrays use [| ... |] delimiters and element access with .(index). Unlike lists, arrays are mutable: items.(1) <- value updates in place. Arrays have O(1) random access; lists have O(n). The Array module provides map, filter, fold, and other operations.

Tuples
pair = [42, "hello"] number, greeting = pair puts number puts greeting
let pair = (42, "hello") let () = let (number, greeting) = pair in Printf.printf "%d " number; print_endline greeting

Tuples are fixed-length, heterogeneous collections — a pair, triple, etc. They are written with parentheses and commas. The type of (42, "hello") is int * string — the * in the type mirrors Cartesian product notation. Pattern matching destructures tuples inline.

Hash tables
scores = { "Alice" => 95, "Bob" => 87 } scores["Carol"] = 92 puts scores["Alice"] puts scores["Carol"]
let () = let scores = Hashtbl.create 4 in Hashtbl.add scores "Alice" 95; Hashtbl.add scores "Bob" 87; Hashtbl.add scores "Carol" 92; Printf.printf "%d " (Hashtbl.find scores "Alice"); Printf.printf "%d " (Hashtbl.find scores "Carol")

OCaml's Hashtbl module provides mutable hash tables. Hashtbl.create takes an initial capacity hint. Hashtbl.find raises Not_found if the key is absent — use Hashtbl.find_opt for a safe version that returns an option.

Control Flow
if/else as an expression
score = 75 grade = if score >= 90 then "A" elsif score >= 80 then "B" elsif score >= 70 then "C" else "D" end puts grade
let score = 75 let grade = if score >= 90 then "A" else if score >= 80 then "B" else if score >= 70 then "C" else "D" let () = print_endline grade

In OCaml, if/else is an expression that returns a value — the same as Ruby's if. Both branches must return the same type; the compiler enforces this. OCaml uses else if (two keywords) rather than Ruby's elsif.

if without else
temperature = 35 puts "Hot day!" if temperature > 30
let temperature = 35 let () = if temperature > 30 then print_endline "Hot day!"

An if without else is valid only when the then branch returns unit (a side effect with no value). If the then branch returned, say, a string, the compiler would require an else branch returning the same type.

for loop
(1..5).each { |counter| puts counter }
let () = for counter = 1 to 5 do Printf.printf "%d " counter done

OCaml's for loop iterates over an integer range (inclusive on both ends) with for variable = start to finish do ... done. The loop variable is immutable inside the body. Use downto instead of to to count downward.

while loop
counter = 1 while counter <= 5 puts counter counter += 1 end
let () = let counter = ref 1 in while !counter <= 5 do Printf.printf "%d " !counter; counter := !counter + 1 done

OCaml's while loop requires explicit mutation via a ref cell because all let bindings are immutable. The ! operator reads the current value and := writes a new one. Idiomatic OCaml prefers recursion or higher-order functions over while loops.

Functional iteration
["alice", "bob", "charlie"].each { |name| puts name.capitalize }
let names = ["alice"; "bob"; "charlie"] let () = List.iter (fun name -> print_endline (String.capitalize_ascii name) ) names

List.iter applies a function to each element for its side effects, discarding the results — equivalent to Ruby's each. String.capitalize_ascii uppercases only the first character. For most iteration over lists, List.iter or List.map replaces explicit loops.

Pattern Matching
Basic match expression
day = 3 day_name = case day when 1 then "Monday" when 2 then "Tuesday" when 3 then "Wednesday" else "Other" end puts day_name
let day = 3 let day_name = match day with | 1 -> "Monday" | 2 -> "Tuesday" | 3 -> "Wednesday" | _ -> "Other" let () = print_endline day_name

match is OCaml's equivalent of Ruby's case/when, but it is an expression that returns a value and is checked for exhaustiveness at compile time. The _ wildcard catches any remaining cases. Each arm is written with | pattern -> expression.

Match with wildcards
status = "error" message = case status when "ok" then "All good" when "error" then "Something failed" else "Unknown status" end puts message
let status = "error" let message = match status with | "ok" -> "All good" | "error" -> "Something failed" | _ -> "Unknown status" let () = print_endline message

String patterns in match use structural equality. The wildcard _ matches any value without binding it to a name. If you want to use the matched value in the arm body, use a variable name instead of _.

Match returns a value
number = 7 description = number.even? ? "even" : "odd" puts description
let number = 7 let description = match number mod 2 with | 0 -> "even" | _ -> "odd" let () = print_endline description

Because match is an expression, its result can be assigned directly to a binding. This is idiomatic OCaml — unlike Ruby's case which is also an expression but is less commonly used that way.

Tuple matching
point = [0, 5] x, y = point location = if x == 0 && y == 0 then "origin" elsif x == 0 then "y-axis" elsif y == 0 then "x-axis" else "quadrant" end puts location
let point = (0, 5) let location = match point with | (0, 0) -> "origin" | (0, _) -> "y-axis" | (_, 0) -> "x-axis" | _ -> "quadrant" let () = print_endline location

Tuples can be matched directly — specific values and wildcards can be mixed freely within a single tuple pattern. The compiler checks all combinations are covered. This replaces a cascade of if/elsif conditions with something the compiler can verify is complete.

List matching
numbers = [1, 2, 3] if numbers.empty? puts "empty" else puts "first: #{numbers.first}, rest has #{numbers.length - 1} elements" end
let numbers = [1; 2; 3] let () = match numbers with | [] -> print_endline "empty" | first :: rest -> Printf.printf "first: %d, rest has %d elements " first (List.length rest)

Lists are matched with [] for the empty list and head :: tail to destructure the first element from the remainder. This mirrors how cons cells are constructed. Recursive functions on lists typically match these two cases.

Guards in match
number = 42 category = if number < 0 then "negative" elsif number == 0 then "zero" elsif number < 100 then "small positive" else "large positive" end puts category
let number = 42 let category = match number with | value when value < 0 -> "negative" | 0 -> "zero" | value when value < 100 -> "small positive" | _ -> "large positive" let () = print_endline category

A when clause adds a guard condition to a pattern arm — the arm only matches if both the pattern and the guard are true. Guard variables are bound from the pattern. If no arm matches (e.g., all guards fail), OCaml raises Match_failure.

Functions
Defining a function
def add(x, y) = x + y puts add(3, 4)
let add x y = x + y let () = Printf.printf "%d " (add 3 4)

Functions in OCaml are defined with let name param1 param2 = body. There are no parentheses around parameters in the definition. Calling a function uses space-separated arguments: add 3 4, not add(3, 4). Parentheses are only needed for grouping.

Currying and partial application
add = ->(first, second) { first + second } add_five = add.curry.call(5) puts add_five.call(3)
let add x y = x + y let add_five = add 5 let () = Printf.printf "%d " (add_five 3)

Every OCaml function is automatically curried — applying it to fewer arguments than it expects returns a new function. There is no need for .curry as in Ruby. Partial application is the natural result of calling a multi-argument function with only some of its arguments.

Anonymous functions
double = ->(number) { number * 2 } puts double.call(5) puts [1, 2, 3].map { |number| number * 2 }.inspect
let double = fun number -> number * 2 let () = Printf.printf "%d " (double 5); let doubled = List.map (fun number -> number * 2) [1; 2; 3] in List.iter (fun number -> Printf.printf "%d " number) doubled; print_newline ()

Anonymous functions are written with fun parameter -> body. They work like Ruby's lambdas or blocks. Multiple parameters: fun x y -> x + y. In OCaml, named functions (let f x = ...) are syntactic sugar for binding a fun expression.

Recursive functions
def factorial(number) return 1 if number <= 1 number * factorial(number - 1) end puts factorial(5)
let rec factorial number = if number <= 1 then 1 else number * factorial (number - 1) let () = Printf.printf "%d " (factorial 5)

OCaml requires the rec keyword to allow a function to call itself — without it, the name is not in scope inside the body. Ruby methods are always recursive implicitly. Use let rec ... and ... to define mutually recursive functions.

Higher-order functions
def apply(value, transform) = transform.call(value) puts apply(5, ->(number) { number * number })
let apply value transform = transform value let () = Printf.printf "%d " (apply 5 (fun number -> number * number))

Functions are first-class values in OCaml: they can be passed as arguments, returned from functions, and stored in data structures. This is the same as Ruby's procs and lambdas, but with static types — the type of apply is inferred as 'a -> ('a -> 'b) -> 'b.

Pipe operator |>
result = [1, 2, 3, 4, 5] .select { |number| number.odd? } .map { |number| number * 3 } .sum puts result
let () = let result = [1; 2; 3; 4; 5] |> List.filter (fun number -> number mod 2 <> 0) |> List.map (fun number -> number * 3) |> List.fold_left (+) 0 in Printf.printf "%d " result

The |> pipe operator (available since OCaml 4.01) threads a value through a sequence of functions: value |> f is f value. It reads left-to-right, like Ruby's method chaining. The operator that inspired F#'s |> and Elixir's |> came from OCaml.

Labeled arguments
def greet(name:, greeting: "Hello") puts "#{greeting}, #{name}!" end greet(name: "Alice") greet(name: "Bob", greeting: "Hi")
let greet ~name ?(greeting = "Hello") () = Printf.printf "%s, %s! " greeting name let () = greet ~name:"Alice" (); greet ~name:"Bob" ~greeting:"Hi" ()

Labeled arguments use a ~label: prefix. Optional arguments use ?(label = default). A trailing () parameter is conventional to force evaluation when optional arguments are present. Labeled arguments can be passed in any order, just like Ruby's keyword arguments.

Function composition
double = ->(number) { number * 2 } increment = ->(number) { number + 1 } double_then_increment = ->(number) { increment.call(double.call(number)) } puts double_then_increment.call(5)
let double number = number * 2 let increment number = number + 1 let double_then_increment number = number |> double |> increment let () = Printf.printf "%d " (double_then_increment 5)

OCaml has no dedicated composition operator (Haskell's . or F#'s >>), but the pipe operator |> achieves composition inline. For point-free composition, you can write let compose f g x = x |> f |> g.

Option Type
The option type
def find_user(name) return nil if name.empty? name.capitalize end user = find_user("alice") puts user.nil? ? "Not found" : user
let find_user name = if String.length name = 0 then None else Some (String.capitalize_ascii name) let () = let user = find_user "alice" in match user with | None -> print_endline "Not found" | Some name -> print_endline name

OCaml's option type makes absence explicit at the type level: a value is either None (absent) or Some value (present). Unlike Ruby's nil, you cannot accidentally call a method on None — the compiler forces you to handle both cases.

Pattern matching on option
values = [nil, 42, nil, 7] values.each do |value| if value.nil? puts "none" else puts value end end
let values = [None; Some 42; None; Some 7] let () = List.iter (fun value -> match value with | None -> print_endline "none" | Some number -> Printf.printf "%d " number ) values

Matching on option is exhaustive — the compiler warns if either None or Some is unhandled. The Some number pattern both tests for presence and binds the contained value in one step, eliminating any need for a null check followed by an unwrap.

Option.map
name = "alice" name_length = name&.length puts name_length
let name = Some "alice" let name_length = Option.map String.length name let () = match name_length with | None -> print_endline "none" | Some length -> Printf.printf "%d " length

Option.map applies a function to the value inside Some, returning None unchanged — analogous to Ruby's safe navigation operator &.. If the option is None, the function is never called.

Option.bind (flat map)
def parse_positive(text) number = Integer(text, exception: false) return nil unless number number > 0 ? number : nil end result = parse_positive("42") puts result
let parse_positive text = match int_of_string_opt text with | None -> None | Some number -> if number > 0 then Some number else None let () = let result = Option.bind (Some "42") parse_positive in match result with | None -> print_endline "invalid" | Some number -> Printf.printf "%d " number

Option.bind opt f applies f to the value inside Some, returning None unchanged — the monadic bind for option. Unlike Option.map, the function f itself returns an option, so the result is not wrapped in an extra Some. Equivalent to Ruby's &.then { ... } when the block can return nil.

Default values with Option.value
name = nil display = name || "Anonymous" puts display
let name = None let display = Option.value name ~default:"Anonymous" let () = print_endline display

Option.value extracts the value from Some, returning the ~default if the option is None. This is the OCaml equivalent of Ruby's || idiom for default values. Option.get extracts without a default and raises Invalid_argument on None.

Variants
Simple variant type
color = :red color_name = case color when :red then "red" when :green then "green" when :blue then "blue" end puts color_name
type color = Red | Green | Blue let color = Red let () = let color_name = match color with | Red -> "red" | Green -> "green" | Blue -> "blue" in print_endline color_name

A variant type (also called a sum type or algebraic data type) defines a closed set of possibilities. Unlike Ruby's symbols, each constructor is a proper typed value — the compiler enforces exhaustive matching and catches typos at compile time.

Variants with data
def area(shape) case shape[:type] when :circle then Math::PI * shape[:radius] ** 2 when :rectangle then shape[:width] * shape[:height] end end printf "%.2f ", area({ type: :circle, radius: 5.0 }) printf "%.2f ", area({ type: :rectangle, width: 4.0, height: 3.0 })
type shape = | Circle of float | Rectangle of float * float let area shape = match shape with | Circle radius -> Float.pi *. radius *. radius | Rectangle (width, height) -> width *. height let () = Printf.printf "%.2f " (area (Circle 5.0)); Printf.printf "%.2f " (area (Rectangle (4.0, 3.0)))

Variant constructors can carry data — Circle of float bundles a float with the tag. Multi-field constructors use tuples: Rectangle of float * float. Pattern matching destructures both the tag and the data in one step, and the compiler checks all cases are covered.

Recursive variant (tree)
# Ruby tree via plain objects def sum_tree(node) return 0 if node.nil? node[:value] + sum_tree(node[:left]) + sum_tree(node[:right]) end tree = { value: 1, left: { value: 2, left: nil, right: nil }, right: { value: 3, left: nil, right: nil } } puts sum_tree(tree)
type tree = | Leaf | Node of int * tree * tree let rec sum_tree t = match t with | Leaf -> 0 | Node (value, left, right) -> value + sum_tree left + sum_tree right let my_tree = Node (1, Node (2, Leaf, Leaf), Node (3, Leaf, Leaf)) let () = Printf.printf "%d " (sum_tree my_tree)

Variants can be self-referential: tree contains tree values. The rec keyword is required on the matching function because it calls itself. Recursive data types paired with recursive functions over them are a fundamental OCaml idiom.

Parametric (generic) variants
# Ruby uses nil/value informally; Sorbet can type this def wrap(value) = value.nil? ? [:nothing] : [:just, value] tag, value = wrap("hello") puts value if tag == :just
(* option is built-in, but we can define an equivalent: *) type 'a maybe = Nothing | Just of 'a let greeting = Just "Hello" let () = match greeting with | Nothing -> print_endline "nothing" | Just message -> print_endline message

The 'a prefix is a type parameter — it makes the variant generic over any type. This is how OCaml's built-in option is defined: type 'a option = None | Some of 'a. A string maybe and an int maybe are different types, checked at compile time.

Complex variant matching
tokens = [42, :plus, 8] result = case tokens[1] when :plus then tokens[0] + tokens[2] when :minus then tokens[0] - tokens[2] end puts result
type token = Number of int | Plus | Minus let evaluate left operator right = match operator with | Plus -> left + right | Minus -> left - right | Number _ -> failwith "not an operator" let () = Printf.printf "%d " (evaluate 42 Plus 8)

Variants make domain modeling precise: a token can only be one of the defined constructors, and the compiler enforces that every case is handled. Using an ADT instead of symbols or strings eliminates whole categories of runtime errors.

Records
Defining and using a record
Person = Struct.new(:name, :age, keyword_init: true) alice = Person.new(name: "Alice", age: 30) puts alice.name puts alice.age
type person = { name : string; age : int; } let alice = { name = "Alice"; age = 30 } let () = print_endline alice.name; Printf.printf "%d " alice.age

OCaml records are immutable typed structs with named fields. Field access uses dot notation, like Ruby's struct accessors. Record fields are separated by semicolons in both the type definition and the construction expression. All fields must be provided at construction time.

Functional record update
alice = { name: "Alice", age: 30 } older_alice = alice.merge(age: 31) puts older_alice[:age]
type person = { name : string; age : int; } let alice = { name = "Alice"; age = 30 } let older_alice = { alice with age = 31 } let () = Printf.printf "%d " older_alice.age

The { record with field = new_value } syntax creates a new record that copies all fields from the original except those explicitly listed. The original is unchanged. This is the OCaml equivalent of Ruby's Hash#merge or a struct's with pattern in other languages.

Pattern matching on records
alice = { name: "Alice", age: 30 } greeting = if alice[:age] < 18 "Hi, #{alice[:name]}! (minor)" else "Hello, #{alice[:name]}! Age: #{alice[:age]}" end puts greeting
type person = { name : string; age : int; } let alice = { name = "Alice"; age = 30 } let greeting = match alice with | { name; age } when age < 18 -> Printf.sprintf "Hi, %s! (minor)" name | { name; age } -> Printf.sprintf "Hello, %s! Age: %d" name age let () = print_endline greeting

Records can be destructured in match patterns: { name; age } binds both fields simultaneously. A when guard can then test the bound values. This combines field access, binding, and conditional logic into a single readable pattern.

Mutable record fields
class Counter attr_accessor :count def initialize = @count = 0 def increment = @count += 1 end counter = Counter.new counter.increment counter.increment puts counter.count
type counter = { mutable count : int; } let counter = { count = 0 } let () = counter.count <- counter.count + 1; counter.count <- counter.count + 1; Printf.printf "%d " counter.count

Individual record fields can be marked mutable, allowing in-place mutation with <-. By default all fields are immutable. This opt-in mutability is different from Ruby, where all instance variables are mutable by default.

Modules
Accessing module functions
puts Math::PI puts [1, 2, 3].length puts "hello".length
let () = Printf.printf "%g " Float.pi; Printf.printf "%d " (List.length [1; 2; 3]); Printf.printf "%d " (String.length "hello")

OCaml's standard library is organized into modules accessed with Module.function. Unlike Ruby, where methods belong to objects, OCaml functions are free-standing values inside module namespaces. The top-level Stdlib module is opened automatically.

Opening a module
include Math puts PI puts sqrt(16.0)
open Printf let () = printf "%.4f " Float.pi; printf "%g " (sqrt 16.0)

open Module brings all of a module's names into scope for the rest of the file — similar to Ruby's include at the module level. sqrt comes from Stdlib, which is always open. Use open sparingly to avoid name shadowing confusion.

Defining a module
module MathHelpers def self.square(number) = number * number def self.cube(number) = number * number * number end puts MathHelpers.square(4) puts MathHelpers.cube(3)
module MathHelpers = struct let square number = number * number let cube number = number * number * number end let () = Printf.printf "%d " (MathHelpers.square 4); Printf.printf "%d " (MathHelpers.cube 3)

A module is defined with module Name = struct ... end. The body contains the same let bindings, type definitions, and other modules that appear at the top level. OCaml modules are first-class and significantly more powerful than Ruby modules — they support functors (higher-order modules) and signatures (interfaces).

Local module open
result = [1, 2, 3].map { |number| number * 2 }.sum puts result
let result = let open List in fold_left (+) 0 (map (fun number -> number * 2) [1; 2; 3]) let () = Printf.printf "%d " result

let open Module in expr opens a module only for the scope of expr, avoiding global namespace pollution. This is useful when writing a block of code that heavily uses one module. An alternative syntax is Module.(expr) for single expressions.

Error Handling
try...with
begin result = Integer("abc") puts result rescue ArgumentError => error puts "Caught: #{error.message}" end
let () = try let result = int_of_string "abc" in Printf.printf "%d " result with Failure message -> Printf.printf "Caught: %s " message

try ... with pattern -> handler catches exceptions. int_of_string raises Failure "int_of_string" on invalid input (use int_of_string_opt for a safe version). Exception patterns use the same syntax as variant patterns.

Custom exceptions
class ValidationError < StandardError; end def validate_age(age) raise ValidationError, "Age must be positive" if age < 0 age end begin validate_age(-1) rescue ValidationError => error puts error.message end
exception ValidationError of string let validate_age age = if age < 0 then raise (ValidationError "Age must be positive") else age let () = try let _ = validate_age (-1) in () with ValidationError message -> print_endline message

Custom exceptions are declared with exception Name of payload_type — they are actually variant constructors of the built-in exn type. The raise function works like Ruby's raise. Exceptions can carry data, just like variant constructors.

Result type
def parse_positive(text) number = Integer(text, exception: false) return [:error, "not a number"] unless number return [:error, "must be positive"] if number <= 0 [:ok, number] end status, value = parse_positive("42") puts value if status == :ok
let parse_positive text = match int_of_string_opt text with | None -> Error "not a number" | Some number -> if number <= 0 then Error "must be positive" else Ok number let () = match parse_positive "42" with | Error message -> print_endline message | Ok number -> Printf.printf "%d " number

OCaml's result type is type ('a, 'e) result = Ok of 'a | Error of 'e. It models operations that can fail without using exceptions. The caller is forced to handle both outcomes. This approach, inspired by OCaml, was later adopted by Rust and other languages.

Result.map
def parse_positive(text) number = Integer(text, exception: false) return nil unless number number > 0 ? number : nil end result = parse_positive("5") doubled = result ? result * 2 : nil puts doubled
let parse_positive text = match int_of_string_opt text with | None -> Error "not a number" | Some number -> if number > 0 then Ok number else Error "must be positive" let () = let result = parse_positive "5" |> Result.map (fun number -> number * 2) in match result with | Error message -> print_endline message | Ok number -> Printf.printf "%d " number

Result.map applies a function to the value inside Ok, leaving Error unchanged — analogous to Option.map. There is also Result.bind for chaining functions that return result. These combinators allow building error-handling pipelines without nested match expressions.

failwith and assert
def get_first(items) raise "List is empty" if items.empty? items.first end begin get_first([]) rescue RuntimeError => error puts error.message end
let get_first items = match items with | [] -> failwith "List is empty" | first :: _ -> first let () = try let _ = get_first ([] : int list) in () with Failure message -> print_endline message

failwith message is shorthand for raise (Failure message) — it raises the standard Failure exception. The type annotation ([] : int list) is needed here only because the empty list's element type cannot be inferred from context. assert false is another common idiom for unreachable branches.