PONYλM2Modula-2
CodeCompared
for OCaml programmers

You already know OCaml.Now explore other languages.

Side-by-side, interactive cheatsheets for OCaml programmers
comparing OCaml to other languages. Every example runs live in your browser — no setup, no installation.

▶ Start with RubyBrowse comparisons ↓Explore the language map ↗

Choose your own path by reordering languages

Ruby⚡ Works Offline⚡ Offline

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.

  • Blocks are the whole language. A closure passed to a method with syntax of its own is why each, times, File.open and every Rails DSL read as they do — and why Enumerable can hand you fifty methods for defining one.
  • Symbols are almost your polymorphic variants — interned, undeclared, compared by identity. What is missing is the type system tracking which ones a value may carry.
  • 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.
  • 🚨 Assignment never copies, and every class is open — including Integer. Two gems can break each other by defining the same method, and the bang suffix (sort!) is the only warning that something mutates.
  • Only nil and false are falsy, which is far cleaner than Python or JavaScript — 0 and "" are truthy, so if value means "is this present".
  • What you gain is Rails, a REPL that inspects a live object graph, a testing culture forced into existence by the missing compiler, and regular expressions and encoding-aware strings in the language rather than in a package.
GoPre-Alpha

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.

  • 🚨 No sum types is the big one. An interface with a marker method plus a type switch is the nearest construction, and it gives neither closedness nor exhaustiveness — adding a case falls silently through to the trailing return.
  • Every type has a zero value, and there is no 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.
  • Errors are values with no ? 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.
  • Interfaces are satisfied implicitly and structurally — closer to your object system than your module system, and the one thing Go does better than almost anyone. Define them at the point of use, small.
  • Goroutines are cheap and select composes: a goroutine per request is normal where a domain per request is not, and timeouts and cancellation are just more channel cases.
  • 🚨 Slices alias, and unpredictably — whether 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.
  • What you gain is operational: one tool that builds, tests, formats, vets and fetches; a race detector and profiler in the box; a single static binary; and net/http already written.
PythonBeta⚡ Works Offline⚡ Offline

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.

  • 🚨 The word "list" means something else. A Python list is a mutable growable array, not an immutable linked list: 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.
  • No currying, no functors, no signatures. 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.
  • Comprehensions and generators are the big ergonomic win — set-builder syntax for lists, dicts, sets and lazy streams, and yield turns ordinary imperative code into a lazy producer.
  • 🚨 A default argument is evaluated ONCE, at definition. A mutable default is shared across every call — the opposite of OCaml's ?(into = []), which is fresh each time. Default to None and build inside.
  • Recursion has a hard limit of about 1000 and no tail calls. Where you would write a tail-recursive helper, write a loop; that is idiomatic here, not a concession.
  • Integers never overflow — arbitrary precision by default, where an OCaml int is 63 bits and wraps silently.
CPre-Alpha

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.

  • Memory is yours. No collector: every allocation has an owner, a lifetime and a matching free, malloc can fail, and ownership is documented in a comment rather than in a type.
  • 🚨 Undefined behavior is a category you have never had. Out-of-bounds access, signed overflow, use-after-free and a null dereference are not errors the language reports — they are situations in which the optimizer may delete your checks. Run the sanitizers.
  • The type system does almost nothing. No inference, no parametric polymorphism, no sum types, no bounds, no nullability. A variant becomes an enum, a union and a struct, and nothing keeps the tag and the payload in step.
  • Arrays decay to pointers, so a length always travels separately and the two can disagree. A string is a pointer plus the NUL-termination convention, and == on strings compares addresses.
  • No closures — a function pointer captures nothing, which is why every C callback API also takes a 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).
  • The FFI is the point. 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.
RustPre-Alpha

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.

  • Traits replace both signatures and functors — an implementation attaches to the type rather than to a module, so no 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.
  • Nothing is curriedadd 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.
  • Tail calls are not guaranteed, so the accumulator-passing recursion that is safe in OCaml can overflow the stack. Where you would write a tail-recursive helper, Rust writes a while loop, and that is idiomatic rather than a concession.
  • One string type becomes two: 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.
  • What Rust genuinely does not have: effect handlers, polymorphic variants, and a persistent Map in the standard library. The first two have no workaround worth the name; the third is a library away.
TypeScriptAlpha⚡ Works Offline⚡ Offline

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.

  • Structural, not nominal. Two record types with the same fields are the same type, with no declaration relating them — so no adapters, and no protection against passing a Fahrenheit where Celsius was wanted.
  • 🚨 The type system is unsound on purpose. 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.
  • There is no 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.
  • Union, intersection and literal types have no OCaml equivalent. string | number holds the plain values with no wrapper, A & B demands both shapes at once, and the string "north" is itself a type.
  • Type-level programming is a whole sub-languagekeyof, 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.
  • One number type, and it is a float. 7 / 2 is 3.5, integers above 2^53 lose precision, and 0.1 + 0.2 is not 0.3.
  • Two null-ish values (null and undefined), tracked separately — but ?. and ?? collapse a chain of absence checks more neatly than Option.bind does.
  • You do not have to leave. js_of_ocaml and Melange compile this same OCaml to the browser, keeping functors, exhaustiveness and sound inference. The reasons to write TypeScript are real and are about colleagues, not the type system.
HaskellPre-Alpha

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.

  • Lazy by default. An argument is a thunk forced only when demanded, so infinite lists are ordinary and Seq is unnecessary — the plain list type already is one. The bill arrives as space leaks: reach for foldl', never foldl.
  • Effects are in the type. A function that prints returns IO () and every caller inherits it, so the pure core and the effectful shell separate themselves instead of by discipline.
  • Type classes replace signatures AND functors — an instance attaches to the type, so the compiler finds it with nothing named at the call site. The price is coherence: one type, one instance, globally, so applying a functor twice with two orderings has no translation.
  • Higher-kinded types are the real capability gap. A class can quantify over a type constructor, which is why one 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.
  • What you keep that Haskell lacks: polymorphic variants, labeled and optional arguments, and a module system whose signatures are first-class. What you gain: comprehensions, sections, deriving, green threads and STM.
F#Pre-Alpha

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.

  • The 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.
  • No functors, at any level. 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.
  • Active patterns are the biggest thing you gain: a function whose name makes it usable as a pattern, so a parse, a regular expression or a computed property can be matched on directly — with exhaustiveness still checked.
  • Computation expressions generalize 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.
  • Units of measure put physical dimensions in the type system and erase them at compile time. 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.
  • Watch the integer width: OCaml's int is 63 bits, F#'s is System.Int32 at 32. The default numeric type gets narrower by half when you cross over.
ReScriptPre-Alpha

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.

  • Functors and polymorphic variants survived the fork — verified against 12.3, not assumed. Among the ML descendants on this anchor, ReScript is the only one that kept OCaml's module system essentially intact.
  • 🚨 [] 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.
  • Application is uncurried since v11, so partial application is explicit: add(10, ...). That is what makes the emitted JavaScript a plain call rather than an arity-adapting closure.
  • Backticks are now string delimiters, so a polymorphic variant tag is #Tag rather than `Tag — and interpolation accepts only strings, so conversions stay explicit.
  • No 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.
  • Binding JavaScript costs one line. @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.
  • What you give up: the native target, GADTs, effect handlers, binding operators, and guaranteed tail calls — no JavaScript engine implements them. If it is OCaml itself you want to keep, Melange compiles real OCaml to the same readable JavaScript.
RocPre-Alpha

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.

  • Tags need no declaration and unions are open — two functions can accept different tag sets with nothing relating them, and exhaustiveness is still checked per match. There is no closed alternative, so this is the sum type.
  • Records are structural as well, so the other half of your type declarations disappears — and Str.inspect, Eq and Hash come free on every type, because the compiler always knows the shape.
  • The platform provides the effects. An application is pure code plugged into a platform chosen at build time, which owns the entry point, the allocator and every effect — so it can target a CLI, a server or a microcontroller with no runtime at all.
  • No garbage collector. Reference counting inserted by the compiler, with no cycles possible because nothing mutates — and when the count is one, an "immutable" update mutates in place and copies nothing.
  • Effects are marked with ! 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.
  • No functors, no macros, no async, and no 1.0. The syntax has changed repeatedly and this site pins one vendored nightly, so the page teaches that build — Try rather than Result, and no ? operator.
  • The error messages are a design goal, and it shows. That is the thing most worth stealing whether or not the language succeeds.
ErlangPre-Alpha

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.

  • No static types at all — no variants, no exhaustiveness, nothing checked before it runs. Dialyzer is a success-typing analyzer that reports no false positives and misses a great deal; -spec is documentation the compiler ignores.
  • 🚨 A variable is bound exactly once. = is a pattern match, not assignment, so your habit of refining a value under one name becomes Value0, Value1, Value2 — a real ergonomic cost.
  • State lives in a process, not a cell. There is no mutable variable of any kind: state is a process tail-calling itself with its next value, reachable only by message. A data race is not expressible.
  • Let it crash — write the happy path and let a process die when reality disagrees, because it is isolated, cheap and supervised. That is a property of the runtime, not a coding style, and adopting it without one is just unhandled exceptions.
  • Selective receive, links, monitors and supervision trees have no OCaml counterpart. Neither does hot code loading, per-process garbage collection with no global pause, or preemption every ~2000 reductions.
  • Bit syntax is the clearest capability gap: a pattern can name field widths in BITS, so a packed protocol header is parsed by writing out its layout.
  • The honest limit: the BEAM is built for scheduling, not arithmetic. CPU-bound numeric work is far slower than native OCaml, and a NIF gets the speed back by reintroducing every risk the platform removes.
Drag cards to reorder · your order is saved locally