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.
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 "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.
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.
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.
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.
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.
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.
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.
# 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.
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.
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.
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.
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 ==.
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.
# 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 =.
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).
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.
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.
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.
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.
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.
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.
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.
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 ==).
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).
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.
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.
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.
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.
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.
(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.
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.
["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.
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.
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 _.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
# 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.
# 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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.