Side-by-side, interactive cheatsheets for OCaml programmers
comparing OCaml to other languages. Every example runs live in your browser — no setup, no installation.
Choose your own path by reordering languages
The opposite bet, made deliberately. Nothing is checked, everything is mutable, every class is open, and a method can be redefined at run time by code you have never read. Ruby is not careless about that — it optimized for the person writing and reading, put expressiveness ahead of proof, and trusted tests and convention to hold the line. The result has the most pleasant closure syntax anywhere and a collection protocol that reads like prose.
each, times, File.open and every Rails DSL read as they do — and why Enumerable can hand you fifty methods for defining one.case/in is real pattern matching, added in Ruby 3.0: it destructures arrays, hashes and objects, binds sub-patterns and takes guards. No exhaustiveness, but an unmatched value raises rather than falling through.Data.define is the record you want — frozen, structurally equal, with a with method. Reaching for it where you would write an OCaml record is the single habit most worth carrying over.Integer. Two gems can break each other by defining the same method, and the bang suffix (sort!) is the only warning that something mutates.nil and false are falsy, which is far cleaner than Python or JavaScript — 0 and "" are truthy, so if value means "is this present".A large step down as a type system, and a defensible trade as a tool for shipping a service. No sum types, no exhaustiveness, no exceptions, no immutability, and generics only since 2022 — every one of those a decision rather than an oversight, made to keep the language small enough that a whole team reads it the same way. What arrives is goroutines, structural interfaces, a compiler that finishes before you look up, one formatting convention, and an HTTP server in the standard library.
option. A missing map key returns 0, an unset field returns 0, and "absent" and "present and zero" are the same question. The value, ok pair is the convention, and nothing forces you to read the ok.? and no bind, so three fallible calls are three if err != nil blocks. Go has declined to add sugar on the grounds that error handling is program logic; the repetition is the price.select composes: a goroutine per request is normal where a domain per request is not, and timeouts and cancellation are just more channel cases.append shares the backing array depends on spare capacity. And nil lives in six types with six behaviors, including the typed-nil interface that is not equal to nil.net/http already written.Everything the compiler was proving for you, you now prove with tests — and in exchange you get the largest library ecosystem there is. Nothing is checked before it runs, there are no algebraic data types, exhaustiveness is never verified, and every container is mutable. What arrives is comprehensions and generators as syntax, a standard library that already has what you need, a REPL that shortens every loop, and colleagues.
append is cheap, prepending is not, len is free, and assignment aliases rather than copies. Stop building results by consing and reversing.None is not option. It is an ordinary value the same name might hold, so nothing forces the absent case open and found + 1 compiles. int | None plus mypy recovers some of it; the interpreter still does not care.match exists and exhaustiveness does not. Python 3.10 structural patterns destructure classes, sequences and mappings nearly as well as OCaml — but delete a case and the match silently falls through, returning None.functools.partial covers partial application; duck typing dissolves the functor entirely, checking nothing where OCaml checked everything. typing.Protocol is the opt-in, checker-only substitute.yield turns ordinary imperative code into a lazy producer.?(into = []), which is fresh each time. Default to None and build inside.int is 63 bits and wraps silently.The layer your language is built on. OCaml's runtime, its collector and every stdlib primitive are C; ocamlopt emits native code that links against it, and the FFI is how OCaml reaches everything already written. This is not a lateral move — it is going down one level, and the reasons are specific: binding a library, understanding what the compiler is doing, or running where a runtime cannot go.
free, malloc can fail, and ownership is documented in a comment rather than in a type.== on strings compares addresses.void *context that the callee casts back, unchecked.#include is a textual paste, not an import, and the preprocessor is the only metaprogramming there is. Hence header guards, and hence macros that break on CIRCLE_AREA(1 + 1).external gives a C function an OCaml type and checks nothing; on the C side, value is tagged so the collector can tell a pointer from an integer — which is exactly why your int is 63 bits — and CAMLparam exists because a moving collector cannot see C locals.The ML type system you already trust, with the garbage collector taken away and replaced by a proof. Algebraic data types, exhaustive matching, Option, immutability by default and let-bindings all arrive nearly unchanged — Rust took them from your language. What is new is ownership: the compiler now tracks who is responsible for freeing every value and how long each reference stays valid, which is the one question OCaml never had to ask you.
Map.Make (String) instantiation is needed and generic code finds the implementation on its own. The cost is coherence: a type implements a trait exactly once, so applying a functor twice with two different orderings has no direct translation.add 10 is not a function waiting for its second argument, and partial application means explicitly returning a closure with move. Point-free pipelines built on |> do not survive the trip; method chaining takes their place.while loop, and that is idiomatic rather than a concession.String owns a heap buffer, &str borrows one, and both are guaranteed UTF-8 in a way OCaml's byte strings never were. The rule that makes it painless is to take &str and return String.Result and ? instead of exceptions — Rust reserves panic! for bugs, so an error a caller should handle is a value, and ? collapses the nested matching that result forces on you in OCaml.Map in the standard library. The first two have no workaround worth the name; the third is a library away.The language your language has been compiling to for fifteen years. ReScript began as an OCaml backend, Melange and js_of_ocaml still compile OCaml to JavaScript, and Flow was written in OCaml. What is genuinely different is that types here are structural, deliberately unsound, and erased without having been proved — and that union, intersection and literal types say things ML's type system cannot.
any, as assertions and untyped data at the boundary let a value carry a type it does not have, and nothing is checked at runtime. Prefer unknown and narrow; treat every as as an unchecked claim.match. switch compares values and nothing more — no guards, no destructuring, no binding. A discriminated union plus a tag field is the substitute, and exhaustiveness must be requested per switch with the never trick.string | number holds the plain values with no wrapper, A & B demands both shapes at once, and the string "north" is itself a type.keyof, typeof, mapped and conditional types compute types from types, so Partial<T> derives rather than repeats. OCaml puts that expressiveness in the module system instead, and TypeScript has no functors at all.7 / 2 is 3.5, integers above 2^53 lose precision, and 0.1 + 0.2 is not 0.3.null and undefined), tracked separately — but ?. and ?? collapse a chain of absence checks more neatly than Option.bind does.The other great ML, and the shared vocabulary hides how differently it thinks. Algebraic data types, exhaustive matching, currying and type inference all carry over — but evaluation is lazy rather than strict, effects are values the type system tracks, and abstraction runs on type classes instead of modules and functors. Three changes, and everything else follows from them.
Seq is unnecessary — the plain list type already is one. The bill arrives as space leaks: reach for foldl', never foldl.IO () and every caller inherits it, so the pure core and the effectful shell separate themselves instead of by discipline.fmap, one >>= and one traverse serve Maybe, lists, Either and IO alike. Your let* has to be defined per type; do does not.:: and : are exactly swapped — : conses and :: annotates. And let is recursive, so let value = value * 2 loops forever rather than shadowing.String is a linked list of Char, one cons cell per character. Real text handling means Data.Text, a qualified import and the OverloadedStrings pragma.deriving, green threads and STM.Your language, ported to .NET, then given twenty years of its own ideas. F# began as an OCaml port, so let, |>, currying, typed format strings, [1; 2; 3], records with with-update, Some/None and match … with all arrive unchanged. What is new runs in both directions: three things are taken away, and three are added that OCaml has no answer for.
in keyword is gone — F# is indentation-sensitive, so a let scopes to the end of its block and a staircase of ins becomes a flat list of lines. ; between statements goes with it.Map.Make (String) becomes a plain generic Map<'Key, 'Value>, which is shorter — but parameterizing a structure over several types and operations at once has no translation, and you restructure around it.let* — seq, async, task and your own builders are all one feature, and a block can contain loops and exception handling that thread through the effect.float<m/s> is a type, and adding meters to seconds does not compile.null arrives through the back door. Types you declare cannot be null, but every .NET type can — including string — with nothing in the signature to say so. Convert at the boundary with Option.ofObj.int is 63 bits, F#'s is System.Int32 at 32. The default numeric type gets narrower by half when you cross over.Your compiler, forked. ReScript began as BuckleScript, a JavaScript backend for the OCaml compiler — and it kept far more of the language than its reputation suggests. Functors, module signatures, polymorphic variants, labeled and optional arguments, exceptions and first-class modules all still work, which is more than F# or Haskell can say. What changed is the syntax, the currying, and the target.
[] is an ARRAY here. A list needs list{}, elements are comma-separated, and the array is the idiomatic collection because that is what JavaScript hands you. Expect to be caught by this more than once.add(10, ...). That is what makes the emitted JavaScript a plain call rather than an arity-adapting closure.#Tag rather than `Tag — and interpolation accepts only strings, so conversions stay explicit.null, no any, no as. The type system is sound, exhaustiveness is automatic on every switch, and the only unsoundness is an external binding you wrote yourself.@val, @module, @send and @get replace the stub file, the dune block and the link step a C FFI needs — and nothing can corrupt a heap.Your most exotic feature, promoted to the default. Roc's tags are structural union types that need no declaration — polymorphic variants, at the center of the language rather than in a corner of it — and its records are structural too. Add a platform/application split that answers the same question effect handlers do, and reference counting with in-place mutation when the count proves it safe, and it is the most interesting design on this anchor.
Str.inspect, Eq and Hash come free on every type, because the compiler always knows the shape.! in the name, enforced by the compiler: a pure function cannot print. And Dec gives exact decimal arithmetic, so 0.1 + 0.2 really is 0.3.Try rather than Result, and no ? operator.Two answers to one question, forty years apart. OCaml 5 has just acquired domains and effect handlers; Erlang has spent thirty-five years on the same problem and arrived somewhere else entirely. So this is not a language that has concurrency where yours does not — it is a different bet about where correctness comes from, and the comparison runs both ways.
-spec is documentation the compiler ignores.= is a pattern match, not assignment, so your habit of refining a value under one name becomes Value0, Value1, Value2 — a real ergonomic cost.