Basics & Output
Hello, World
Elm renders a pure
main value. An OCaml program is a sequence of top-level items executed in order; let () = … runs an expression for its side effect.module Main exposing (main)
import Html exposing (text)
main =
text "Hello, World!"
let () = print_endline "Hello, World!" You are meeting the language your language came from: Elm's syntax family —
let, whitespace application, arrow types — is OCaml's, via Haskell and F#. The let () = idiom pattern-matches the unit result of print_endline, quietly asserting the expression returns () — a tiny taste of how deeply matching runs here.Bindings & inference
OCaml invented the inference Elm relies on. Annotations are optional in both, inline after a colon when you want them.
module Main exposing (main)
import Html exposing (text)
greeting : String
greeting =
"Hello from Elm"
answer =
42
main =
text (greeting ++ " " ++ String.fromInt answer)
let greeting : string = "Hello from OCaml"
let answer = 42
let () = Printf.printf "%s %d\n" greeting answer Hindley–Milner inference has been running this show since 1978 —
answer needs no annotation in either language. Printf.printf's format string is type-checked at compile time: %d demands an integer, %s a string, and a mismatch is a compile error, not a runtime surprise.Bindings & Shadowing
Shadowing is allowed
Elm forbids reusing a name in scope. OCaml happily lets a new
let shadow an old one — a common, idiomatic pattern.module Main exposing (main)
import Html exposing (text)
counter : Int
counter =
0
incremented : Int
incremented =
counter + 1
main =
text (String.fromInt incremented)
let count = 0
let count = count + 1
let count = count + 1
let () = Printf.printf "%d\n" count Each
let count = count + 1 defines a new immutable binding whose initializer sees the previous one — no mutation happens, but the name is reused. Elm rejected shadowing to keep every name unambiguous; OCaml embraces it as pipeline-style rebinding. Watch for it when reading OCaml: the count on line three is not the count on line one.Real mutation: ref cells
When OCaml actually mutates, it says so in the type: a
ref is a mutable box, written with := and read with !.module Main exposing (main)
import Html exposing (text)
-- Elm has no mutable state at all;
-- a "changed" value is a new value.
total : Int
total =
List.foldl (+) 0 [ 1, 2, 3 ]
main =
text (String.fromInt total)
let total = ref 0
let () =
List.iter (fun number -> total := !total + number) [ 1; 2; 3 ];
Printf.printf "%d\n" !total The
! prefix dereferences (nothing to do with boolean not, which is not), and := stores. Functional style remains the default — a fold would be more idiomatic here — but OCaml treats imperative code as a first-class tool rather than a sin, which is the big philosophical divergence from Elm.Numbers & Operators
Float operators wear a dot
Like ReScript (which inherited it from here) and unlike Elm's polymorphic
number, OCaml splits arithmetic: + - * / for integers, +. -. *. /. for floats.module Main exposing (main)
import Html exposing (text)
main =
text (String.fromInt (7 // 2) ++ " and " ++ String.fromFloat (7 / 2))
let () = Printf.printf "%d\n" (7 / 2)
let () = Printf.printf "%f\n" (7.0 /. 2.0)
let () = Printf.printf "%f\n" (float_of_int 7 /. 2.0) Integer
/ truncates — it is Elm's // — and there is no implicit conversion in either language, so float_of_int does explicitly what Elm's two division operators sidestep. The dotted operators are the price OCaml pays for full inference without type classes: the operator itself pins the type.Strings & Printf
Concatenation is ^
A third concatenation spelling to file next to Elm's
++: OCaml glues strings with ^, and has no interpolation — Printf fills that role.module Main exposing (main)
import Html exposing (text)
name : String
name =
"world"
main =
text ("Hello, " ++ name ++ "! " ++ String.fromInt (6 * 7))
let name = "world"
let () = print_endline ("Hello, " ^ name ^ "!")
let () = Printf.printf "Hello, %s! %d\n" name (6 * 7) Like Elm, OCaml never coerces a number into a string — you convert with
string_of_int or reach for Printf, whose type-checked format string is the closest thing to interpolation. Elm's ++ habits transfer directly; only the symbol changes.The String module
module Main exposing (main)
import Html exposing (text)
greeting : String
greeting =
"hello world"
main =
text (String.toUpper greeting ++ " / " ++ String.fromInt (String.length greeting))
let greeting = "hello world"
let () = print_endline (String.uppercase_ascii greeting)
let () = Printf.printf "%d\n" (String.length greeting) Same module-of-functions organization —
String.length is even spelled identically. The _ascii suffix on uppercase_ascii is honest labeling: the standard library's case functions only handle ASCII, and real Unicode work uses an external library, where Elm's String.toUpper is Unicode-aware out of the box.Functions & Currying
Currying works exactly like home
OCaml is where automatic currying (of the ML line Elm sits in) comes from: every function takes one argument, and partial application is free.
module Main exposing (main)
import Html exposing (text)
add : Int -> Int -> Int
add first second =
first + second
addTen : Int -> Int
addTen =
add 10
main =
text (String.fromInt (addTen 5))
let add first second = first + second
let add_ten = add 10
let () = Printf.printf "%d\n" (add_ten 5) Identical shape, identical semantics, identical inferred type —
int -> int -> int. Unlike ReScript (which abandoned currying for JS-friendly output), OCaml keeps the full ML discipline, so an Elm developer's partial-application instincts work unmodified. Naming flips to snake_case by convention.Anonymous functions
module Main exposing (main)
import Html exposing (text)
main =
text (Debug.toString (List.map (\number -> number * 3) [ 1, 2, 3 ]))
let tripled = List.map (fun number -> number * 3) [ 1; 2; 3 ]
let () = List.iter (Printf.printf "%d ") tripled
let () = print_newline () Elm's
\number -> is fun number -> — F# kept the same keyword. Notice List.map takes the function first and the list last, curried, exactly as in Elm; the partial application Printf.printf "%d " handed to List.iter shows the style paying off.The pipe lives here too
OCaml has
|> in its standard library, and because functions are curried data-last, it behaves exactly like Elm's.module Main exposing (main)
import Html exposing (text)
main =
[ 1, 2, 3, 4, 5, 6 ]
|> List.filter (\number -> modBy 2 number == 0)
|> List.map (\number -> number * number)
|> List.sum
|> String.fromInt
|> text
let () =
[ 1; 2; 3; 4; 5; 6 ]
|> List.filter (fun number -> number mod 2 = 0)
|> List.map (fun number -> number * number)
|> List.fold_left (+) 0
|> Printf.printf "%d\n" Data-last currying is what makes this work: each stage is a partially applied function waiting for the list. Dialect notes: equality is a single
=, modulo is the infix mod keyword, and Elm's List.sum is spelled as a fold — List.fold_left (+) 0, passing the operator itself.Recursion needs `rec`
module Main exposing (main)
import Html exposing (text)
factorial : Int -> Int
factorial number =
if number <= 1 then
1
else
number * factorial (number - 1)
main =
text (String.fromInt (factorial 5))
let rec factorial number =
if number <= 1 then 1
else number * factorial (number - 1)
let () = Printf.printf "%d\n" (factorial 5) The
rec keyword is OCaml's own rule (F# and ReScript inherited it): without it, the body's factorial would refer to some earlier binding rather than the one being defined — which is precisely what makes deliberate shadowing possible. Elm, having banned shadowing, could make every definition recursive by default.Lists, Arrays & Tuples
Lists: semicolons, and the comma trap
OCaml list literals separate elements with semicolons — and Elm-style commas silently build a one-element list containing a tuple.
module Main exposing (main)
import Html exposing (text)
numbers : List Int
numbers =
[ 1, 2, 3 ]
main =
text (Debug.toString (1 :: numbers))
let numbers = [ 1; 2; 3 ]
let longer = 1 :: numbers
let () = List.iter (Printf.printf "%d ") longer
let () = print_newline () F# inherited both the semicolons and the trap from here:
[ 1, 2, 3 ] type-checks as [(1, 2, 3)], a single-element list holding a triple. The cons operator :: is Elm's own, unchanged, and works in patterns the same way.The List module
module Main exposing (main)
import Html exposing (text)
main =
[ 1, 2, 3, 4 ]
|> List.map (\number -> number * 10)
|> List.filter (\number -> number > 15)
|> Debug.toString
|> text
let () =
[ 1; 2; 3; 4 ]
|> List.map (fun number -> number * 10)
|> List.filter (fun number -> number > 15)
|> List.iter (Printf.printf "%d ")
let () = print_newline () The vocabulary maps almost one to one:
List.map, List.filter, List.length, List.rev; Elm's List.foldl is List.fold_left. Both are the same immutable singly-linked list underneath — cheap to prepend, walked to append.Arrays are mutable
OCaml's
[| … |] array is fixed-length and mutable in place — a genuinely different animal from Elm's persistent Array.module Main exposing (main)
import Html exposing (text)
main =
text (Debug.toString [ 10, 20, 30 ])
let numbers = [| 10; 20; 30 |]
let () = numbers.(0) <- 99
let () = Array.iter (Printf.printf "%d ") numbers
let () = print_newline () Indexing is
numbers.(0) and in-place update is <- — no new array is allocated, which Elm simply cannot express. F#'s [| |] syntax came from here. Idiomatic OCaml uses lists for everyday data and arrays when performance or fixed-size mutation matters.Tuples
module Main exposing (main)
import Html exposing (text)
point : ( Int, String )
point =
( 1, "one" )
main =
text (String.fromInt (Tuple.first point) ++ " is " ++ Tuple.second point)
let point = (1, "one")
let (number, name) = point
let () = Printf.printf "%d is %s\n" number name
let () = Printf.printf "%d\n" (fst point) The type is written
int * string — the product-type star Elm replaced with parentheses. Destructuring in a let works as in Elm, and fst/snd are Tuple.first/Tuple.second.Records
Records: declared, then matched
OCaml records need a type declaration (they are nominal, like F#'s and ReScript's — Elm's structural records are the family outlier), and they pattern-match.
module Main exposing (main)
import Html exposing (text)
type alias Person =
{ name : String
, age : Int
}
person : Person
person =
{ name = "Ada", age = 36 }
main =
text (person.name ++ " is " ++ String.fromInt person.age)
type person = { name : string; age : int }
let person = { name = "Ada"; age = 36 }
let { name; age } = person
let () = Printf.printf "%s is %d\n" name age Field punning —
let { name; age } = person — destructures a record by field name, something Elm also allows in function arguments. Dot access person.name works too. The literal finds its type by its field names, so the declaration usually sits quietly at the top of the file.Record update with `with`
module Main exposing (main)
import Html exposing (text)
type alias Person =
{ name : String
, age : Int
}
person : Person
person =
{ name = "Ada", age = 36 }
older : Person
older =
{ person | age = 37 }
main =
text (older.name ++ " is now " ++ String.fromInt older.age)
type person = { name : string; age : int }
let person = { name = "Ada"; age = 36 }
let older = { person with age = 37 }
let () = Printf.printf "%s is now %d\n" older.name older.age
let () = Printf.printf "%s is still %d\n" person.name person.age This is the original — F# copied
{ person with age = 37 } verbatim, and Elm's { person | age = 37 } is the same construct respelled. A new record shares the unchanged fields; the original is untouched, as the second print shows.Variants
Custom types are variants
Elm's custom types are OCaml's variants — the construct Elm inherited most directly, payloads introduced with
of.module Main exposing (main)
import Html exposing (text)
type Shape
= Circle Float
| Rectangle Float Float
area : Shape -> Float
area shape =
case shape of
Circle radius ->
3.14159 * radius * radius
Rectangle width height ->
width * height
main =
text (String.fromFloat (area (Rectangle 3 4)))
type shape =
| Circle of float
| Rectangle of float * float
let area shape =
match shape with
| Circle radius -> 3.14159 *. radius *. radius
| Rectangle (width, height) -> width *. height
let () = Printf.printf "%f\n" (area (Rectangle (3.0, 4.0))) A multi-value payload is one tuple (
Rectangle (3.0, 4.0)) rather than Elm's curried constructor arguments — F# kept that too. Exhaustiveness checking behaves like Elm's: leave out a constructor and the compiler tells you which one.Type parameters, backwards
OCaml writes its type parameters before the type name: Elm's
Tree a is 'a tree.module Main exposing (main)
import Html exposing (text)
type Tree a
= Leaf
| Node (Tree a) a (Tree a)
size : Tree a -> Int
size tree =
case tree of
Leaf ->
0
Node left _ right ->
1 + size left + size right
main =
text (String.fromInt (size (Node (Node Leaf 1 Leaf) 2 (Node Leaf 3 Leaf))))
type 'value tree =
| Leaf
| Node of 'value tree * 'value * 'value tree
let rec size tree =
match tree with
| Leaf -> 0
| Node (left, _, right) -> 1 + size left + size right
let () = Printf.printf "%d\n" (size (Node (Node (Leaf, 1, Leaf), 2, Node (Leaf, 3, Leaf)))) Postfix type application reads like English backwards from Elm:
int list, string option, 'value tree. It is the one piece of ML syntax Elm's designers reversed. The tick on 'value is the traditional type-variable marker Elm dropped.Pattern Matching
case…of becomes match…with
module Main exposing (main)
import Html exposing (text)
describe : List Int -> String
describe numbers =
case numbers of
[] ->
"empty"
[ single ] ->
"one: " ++ String.fromInt single
first :: _ ->
"starts with " ++ String.fromInt first
main =
text (describe [ 7, 8, 9 ])
let describe numbers =
match numbers with
| [] -> "empty"
| [ single ] -> "one: " ^ string_of_int single
| first :: _ -> "starts with " ^ string_of_int first
let () = print_endline (describe [ 7; 8; 9 ]) Everything transfers: empty-list and exact-length patterns, cons, wildcards, exhaustiveness warnings naming the missing cases. OCaml marks branches with a leading
| where Elm uses indentation — this is the construct case … of was modeled on.Guards with when
module Main exposing (main)
import Html exposing (text)
describe : Int -> String
describe number =
if number == 0 then
"zero"
else if number < 0 then
"negative"
else
"positive"
main =
text (describe (-4))
let describe number =
match number with
| 0 -> "zero"
| value when value < 0 -> "negative"
| _ -> "positive"
let () = print_endline (describe (-4)) The
when guard is the ancestor of F#'s, with the same trade Elm opted out of: arbitrary conditions on a pattern, at the cost of the compiler no longer proving the guarded cases exhaustive — hence the mandatory final wildcard.option & result
Maybe is option
Maybe a is OCaml's 'a option — Some and None, with an Option module of helpers to match.module Main exposing (main)
import Html exposing (text)
firstEven : Maybe Int
firstEven =
List.filter (\number -> modBy 2 number == 0) [ 1, 3, 4, 6 ]
|> List.head
main =
firstEven
|> Maybe.map (\number -> number * 10)
|> Maybe.withDefault 0
|> String.fromInt
|> text
let first_even = List.find_opt (fun number -> number mod 2 = 0) [ 1; 3; 4; 6 ]
let () =
first_even
|> Option.map (fun number -> number * 10)
|> Option.value ~default:0
|> Printf.printf "%d\n" Maybe.map is Option.map, Maybe.withDefault is Option.value ~default:… (that tilde marks a labeled argument, an OCaml feature Elm never had), and the _opt suffix on List.find_opt signals the safe variant — because, as the exceptions section shows, the unsuffixed List.find throws.result, same but flipped
module Main exposing (main)
import Html exposing (text)
safeDivide : Float -> Float -> Result String Float
safeDivide numerator denominator =
if denominator == 0 then
Err "cannot divide by zero"
else
Ok (numerator / denominator)
main =
case safeDivide 10 2 of
Ok quotient ->
text (String.fromFloat quotient)
Err reason ->
text reason
let safe_divide numerator denominator =
if denominator = 0.0 then Error "cannot divide by zero"
else Ok (numerator /. denominator)
let () =
match safe_divide 10.0 2.0 with
| Ok quotient -> Printf.printf "%f\n" quotient
| Error reason -> print_endline reason OCaml's
('ok, 'error) result puts the success type first where Elm writes Result error value, and spells Err as Error. Result.map and Result.bind chain exactly like Result.map/Result.andThen.Exceptions
Exceptions are idiomatic here
Not just present but woven in: core library functions like
List.find and Hashtbl.find raise on failure, with _opt variants as the safe alternatives.module Main exposing (main)
import Html exposing (text)
-- Elm's only failure channel is a value.
found : Maybe Int
found =
List.head (List.filter (\number -> number > 10) [ 1, 2, 3 ])
main =
text (Debug.toString found)
let () =
match List.find (fun number -> number > 10) [ 1; 2; 3 ] with
| found -> Printf.printf "%d\n" found
| exception Not_found -> print_endline "nothing over 10" The
exception pattern inside match (an OCaml 4.02 gem) handles the raise in the same expression that handles success. This is the deepest cultural difference from Elm: OCaml's standard library predates the option-everywhere consensus, so you must know which functions throw. Modern OCaml style leans toward the _opt variants — an Elm developer's instincts pointing the right way.Imperative OCaml & Modules
An actual for loop
Elm famously has no loops. OCaml — a functional language with imperative features, not a pure one — has real
for and while.module Main exposing (main)
import Html exposing (text)
main =
List.range 1 5
|> List.map String.fromInt
|> String.join " "
|> text
let () =
for number = 1 to 5 do
Printf.printf "%d " number
done;
print_newline () The loop body must return
() — it exists purely for side effects, which is why Elm (having none) never needed the construct. In practice OCaml programmers still reach for List.map and folds first; the loop is there for the cases where an index and mutation genuinely read better.Modules are a language of their own
Elm modules are namespaces — one per file. OCaml modules are first-class structures: defined inline, nested, given signatures, even parameterized over other modules (functors).
module Main exposing (main)
import Html exposing (text)
-- An Elm module is exactly one file with an exposing list;
-- there is no inline module, no signature, no functor.
circleArea : Float -> Float
circleArea radius =
3.14159 * radius * radius
main =
text (String.fromFloat (circleArea 2))
module Geometry = struct
let pi = 3.14159
let circle_area radius = pi *. radius *. radius
end
let () = Printf.printf "%f\n" (Geometry.circle_area 2.0) This inline
module … = struct … end barely hints at the system: signatures hide implementation details the way Elm's exposing lists do, and functors — functions from modules to modules — power things like Map.Make, generating a whole map implementation from a comparison module. It is OCaml's most celebrated feature and has no Elm counterpart at all.