Hello World & the Build
Hello, World
F# needs no
let () = wrapper: top-level code in the last file of a project simply runs, in order.let () = print_endline "Hello, World!"printfn "Hello, World!"OCaml's
let () = … is a pattern binding that both runs the expression and asserts it has type unit. F# keeps the same underlying rule — a top-level expression must be unit — but lets you write the expression on its own, which reads as a statement even though it is not one. printfn is Printf.printf with a newline appended; printf without the n is the exact analogue.Typed Format Strings
F# inherited OCaml's typed format strings wholesale, directives and all — this is one of the few places any two languages share a feature this unusual.
let name = "OCaml"
let year = 1996
let () = Printf.printf "%s appeared in %d\n" name yearlet name = "F#"
let year = 2005
printfn "%s appeared in %d" name yearIn both languages the literal is not a string but a value of a special
format type, and the compiler reads its directives to determine what arguments printf demands. %s, %d, %f and %b mean the same things in both. F# adds %A, which pretty-prints any value structurally — records, unions, lists and all — and is the closest thing to a built-in debugger print. OCaml has no equivalent without a ppx deriver.dune and opam vs the .fsproj
Configuration rather than code, so neither column runs. One line in the F# column has no OCaml counterpart and catches every newcomer.
(* dune-project *)
(lang dune 3.16)
(* bin/dune *)
(executable
(name main)
(libraries str))
(* Build and run:
dune build
dune exec bin/main.exe
Dependencies come from opam. *)// demo.fsproj
// <Project Sdk="Microsoft.NET.Sdk">
// <PropertyGroup>
// <OutputType>Exe</OutputType>
// <TargetFramework>net10.0</TargetFramework>
// </PropertyGroup>
// <ItemGroup>
// <Compile Include="Library.fs" />
// <Compile Include="Program.fs" />
// </ItemGroup>
// </Project>
// Build and run:
// dotnet build
// dotnet run
//
// Dependencies come from NuGet: dotnet add package FSharp.DataThe
<Compile Include> list is ordered, and the order is significant: F# compiles files strictly top to bottom, and a file may only reference definitions from files above it. There is no equivalent of OCaml's dependency-resolving build, where dune works out the order from the module graph. This forces a linear architecture on every F# project and is the single most surprising constraint for someone arriving from OCaml. On the other hand, dotnet is both package manager and build tool, where OCaml splits the job between opam and dune.The Entry Point and Exit Code
Both languages run top-level code in order. F# additionally offers a named entry point, and it must be the last thing in the last file.
(* OCaml runs top-level bindings in order; the exit code
is 0 unless something raises or exit is called. *)
let () =
print_endline "working";
exit 0// Top-level code runs the same way, but a project can also
// declare an explicit entry point that returns the exit code.
[<EntryPoint>]
let main argv =
printfn "working"
0The
[<EntryPoint>] attribute marks a function taking a string[] of command-line arguments and returning the process exit code as an int. It is optional — top-level code works, as every other example on this page shows — but it is the only way to receive the arguments without reaching for System.Environment.GetCommandLineArgs, and returning a non-zero code from it is cleaner than calling exit. OCaml has no such declaration: Sys.argv is a global array, and the exit code comes from exit or from falling off the end.Layout: Losing in and ;;
Indentation Replaces in
F# is indentation-sensitive. The scope of a
let runs to the end of the enclosing block, so the in keyword is gone.let () =
let count = 10 in
let label = "items" in
let summary = Printf.sprintf "%d %s" count label in
print_endline summarylet count = 10
let label = "items"
let summary = sprintf "%d %s" count label
printfn "%s" summaryThis is the largest syntactic change between the two languages, and it is almost entirely a subtraction. OCaml's
let … in is an expression that nests, which is why a sequence of bindings becomes a staircase of ins. F# uses the offside rule instead: a let is in scope for everything indented at or beyond its own column. F# does still accept let … in on one line, and its "verbose syntax" accepts the OCaml spelling wholesale, but neither is idiomatic and nobody writes them.Sequencing Statements
OCaml separates expressions in a sequence with
;. F# uses a newline at the same indentation and reserves ; for one-liners.let () =
print_endline "first";
print_endline "second";
print_endline "third"printfn "first"
printfn "second"
printfn "third"OCaml's
; is an operator meaning "evaluate the left side for its effect, discard it, then evaluate the right side," and it is required. F# treats a newline at the same column as exactly that, so the semicolons vanish. The trap for an OCaml programmer is the opposite direction: a stray ; at the end of an F# line is legal and does nothing, but a stray one in OCaml where you meant ;; — or a missing one — produces type errors about unit that name a line far from the mistake.A Nested Scope
Inside a function body the two languages line up closely — the F# version is the OCaml version with the
ins deleted.let describe values =
let count = List.length values in
let total = List.fold_left ( + ) 0 values in
Printf.sprintf "%d values, total %d" count total
let () = print_endline (describe [ 4; 8; 15 ])let describe values =
let count = List.length values
let total = List.fold ( + ) 0 values
sprintf "%d values, total %d" count total
printfn "%s" (describe [ 4; 8; 15 ])The final expression of an F# function body is its return value, exactly as in OCaml. The one name change visible here is
List.fold_left becoming List.fold; F# also has List.foldBack for what OCaml calls fold_right, and its standard library uses camelCase for multi-word names throughout (List.tryFind, String.length, Option.defaultValue) where OCaml uses snake_case.Variables & Types
Type Inference
Both languages run Hindley–Milner inference over the whole program, so neither column needs a single annotation.
let add first second = first + second
let describe items = List.length items
let () =
Printf.printf "%d\n" (add 3 4);
Printf.printf "%d\n" (describe [ "a"; "b"; "c" ])let add first second = first + second
let describe items = List.length items
printfn "%d" (add 3 4)
printfn "%d" (describe [ "a"; "b"; "c" ])The columns are identical apart from the printing. Both infer
add : int -> int -> int and both generalize describe over the element type. The one behavioral difference to know about is that F# infers left to right, top to bottom and cannot use information from later in the file — a consequence of the ordered compilation the project file imposes. OCaml solves constraints across the whole unit, so it happily infers a type from a use that appears after the definition.ref vs let mutable
F# has a real mutable binding, so the ref cell OCaml needs for a counter is gone.
let () =
let total = ref 0 in
List.iter (fun number -> total := !total + number) [ 1; 2; 3; 4 ];
Printf.printf "%d\n" !totallet mutable total = 0
for number in [ 1; 2; 3; 4 ] do
total <- total + number
printfn "%d" totalOCaml's
ref is an ordinary record with one mutable field, so a counter costs an allocation and every read costs a dereference. F#'s let mutable is a genuine mutable location, and <- assigns to it. F# also has ref cells with the same := and .Value spelling, kept for the cases where a ref cell must be captured by a closure or escape its scope — but for a local counter let mutable is both faster and what everyone writes.Integer Widths
This is a genuine portability hazard between the two, and it goes in the direction most people do not expect.
(* OCaml's int is 63 bits: one bit is the GC's value tag. *)
let () =
Printf.printf "max_int = %d\n" max_int;
Printf.printf "63-bit? %b\n" (max_int = (1 lsl 62) - 1)// F#'s int is System.Int32 — a full 32 bits, and no wider.
printfn "max = %d" System.Int32.MaxValue
printfn "32-bit? %b" (System.Int32.MaxValue = (1 <<< 30) - 1 + (1 <<< 30))OCaml's
int is 63 bits on a 64-bit platform, because one bit is spent on the tag the garbage collector reads. F#'s int is System.Int32 — 32 bits, because that is what .NET's int is. So the default integer type gets narrower by half when you cross over, and code that fits comfortably in an OCaml int can overflow an F# one. The 64-bit type is int64, written with an L suffix (42L). Note also that F# spells the shift operators <<< and >>> where OCaml writes lsl and lsr.Tuples
Tuples and destructuring are the same in both; only the modulus operator and the naming convention change.
let divide_and_remainder numerator denominator =
(numerator / denominator, numerator mod denominator)
let () =
let (quotient, remainder) = divide_and_remainder 17 5 in
Printf.printf "%d remainder %d\n" quotient remainderlet divideAndRemainder numerator denominator =
(numerator / denominator, numerator % denominator)
let (quotient, remainder) = divideAndRemainder 17 5
printfn "%d remainder %d" quotient remainderOCaml writes
mod as a keyword-like infix operator; F# uses %, following .NET. The naming difference is a convention rather than a rule: F# uses camelCase for values and functions and PascalCase for types, modules and namespaces, where OCaml uses snake_case for values and capitalizes modules and constructors. F# will happily accept divide_and_remainder, but every library you call will be camelCase, so mixing the two conventions in one file reads badly.Shadowing
Shadowing is idiomatic in both languages — but F# permits it in strictly fewer places than OCaml, and the restriction is easy to trip over.
(* OCaml allows shadowing at EVERY level, including the
top level of a file. *)
let value = 5
let value = value * 2
let value = string_of_int value ^ " points"
let () = print_endline value// F# allows it only inside a nested scope. The same three
// lines at MODULE level are error FS0037, "Duplicate
// definition of value".
let describe () =
let value = 5
let value = value * 2
let value = string value + " points"
value
printfn "%s" (describe ())Inside a function body the two are the same: each
let creates a fresh binding that hides the previous one, nothing is mutated, and the type may change on the way through. At the top level of a module they diverge. OCaml is happy — the anchor column above shadows value twice at file scope. F# rejects it with error FS0037: Duplicate definition of value, because a module-level let compiles to a static member of a class and two members cannot share a name. So the OCaml habit of refining a top-level binding step by step has to move inside a function. F# also names the int-to-string conversion just string, where OCaml spells out string_of_int.Operators & Overloading
The Float Operators Disappear
One of the most visible day-to-day differences, and one of the very few places F# is unambiguously more pleasant.
(* OCaml has separate operators for floats: +. -. *. /. *)
let () =
let area = 3.0 *. 4.0 in
let total = 1 + 2 in
Printf.printf "%.1f\n" area;
Printf.printf "%d\n" total// F# overloads one set of operators across numeric types.
let area = 3.0 * 4.0
let total = 1 + 2
printfn "%.1f" area
printfn "%d" totalOCaml has no operator overloading at all, so
+ is int -> int -> int and floats need their own family: +., -., *., /. and **. F# resolves * by the types of its operands, so one spelling serves every numeric type. The cost is that F# cannot always infer which overload you meant from context alone — a function written as let double x = x * 2 is inferred as int -> int and will not accept a float, where OCaml's *. would have made the intent explicit. When you want a genuinely numeric-generic function, F# needs inline.Concatenation
The same overloading that unified the numeric operators also absorbs OCaml's
^.let () =
let greeting = "Hello" ^ ", " ^ "world" in
print_endline greetinglet greeting = "Hello" + ", " + "world"
printfn "%s" greetingOCaml reserves
^ for string concatenation because + is already taken by integers. F# overloads + for System.String, so ^ is unnecessary — though F# still accepts it, for exactly this reason. The idiomatic F# spelling for anything longer than a couple of pieces is string interpolation, $"Hello, {subject}", which has no OCaml equivalent short of Printf.sprintf.Defining an Operator
Custom infix operators work identically, down to the spacing inside the parentheses.
let ( +++ ) left right = left * 10 + right
let () = Printf.printf "%d\n" (4 +++ 2)let ( +++ ) left right = left * 10 + right
printfn "%d" (4 +++ 2)Both languages let you define an operator from a set of symbol characters, and both derive its precedence and associativity from its first character — so
+++ binds like + in either language, with no way to declare otherwise. This is one of the details F# copied exactly rather than adapting. The practical difference is cultural: OCaml code defines operators freely (|>, @@, let*), while F# convention discourages inventing new ones outside a library's core vocabulary.Strings
Bytes vs UTF-16
The same five-letter word gives two different lengths, and the reason is the deepest change the .NET runtime makes to a familiar type.
let () =
let text = "caffè" in
Printf.printf "length = %d\n" (String.length text);
Printf.printf "first = %c\n" text.[0]let text = "caffè"
printfn "length = %d" text.Length
printfn "first = %c" text.[0]An OCaml
string is an immutable sequence of bytes with no declared encoding, so this word occupies six of them and String.length answers 6. An F# string is System.String, a sequence of UTF-16 code units, so the same word is five units and .Length answers 5. Neither is a count of characters in the general case — an emoji is two UTF-16 units and four UTF-8 bytes — but F#'s number is at least close for European text where OCaml's is not. Indexing looks identical and means something different in each: a byte in OCaml, a code unit in F#.Interpolation
F# has interpolated strings; OCaml has never grown them, and
sprintf remains the answer.(* OCaml has no interpolation syntax. sprintf is the tool. *)
let () =
let name = "Ada" in
let age = 36 in
print_endline (Printf.sprintf "%s is %d" name age)let name = "Ada"
let age = 36
printfn "%s" $"{name} is {age}"F#'s
$"…" embeds any expression in braces and calls ToString() on the result. A typed variant exists too — $"%s{name} is %d{age}" — which reintroduces the compile-time checking of a format string while keeping the values inline, and is the best of both. OCaml's only comparable option is a ppx extension such as ppx_string, which is a build-system change rather than a language feature.Splitting and Joining
The operations correspond, but note where each one lives — this row is really about F# having two standard libraries.
let () =
let line = "alpha,beta,gamma" in
let parts = String.split_on_char ',' line in
List.iter (fun part -> Printf.printf "<%s>" part) parts;
print_newline ();
print_endline (String.concat " | " parts)let line = "alpha,beta,gamma"
let parts = line.Split(',')
for part in parts do
printf "<%s>" part
printfn ""
printfn "%s" (String.concat " | " parts)line.Split(',') is a method call on System.String, inherited from .NET, returning a string[]. String.concat is an F# module function, matching OCaml's. Every F# program mixes the two vocabularies, and knowing which is which saves a lot of searching: the F# modules (List, Array, Seq, String, Option) are curried, take the data last and compose with |>; the .NET methods are uncurried, are called with dot notation and do not. When both offer the same operation, prefer the F# module function for pipelines.Control Flow
if Is an Expression
Both make
if an expression and require every branch to agree on a type. F# adds one keyword OCaml does not have.let () =
let temperature = 31 in
let advice =
if temperature > 30 then "stay inside"
else if temperature > 20 then "pleasant"
else "bring a coat"
in
print_endline advicelet temperature = 31
let advice =
if temperature > 30 then "stay inside"
elif temperature > 20 then "pleasant"
else "bring a coat"
printfn "%s" adviceelif is F#'s contraction of else if, and it exists because the offside rule makes a chain of nested else if blocks drift rightward. Both languages apply the same rule about a missing else: the if must then have type unit, because there is no value to produce when the condition is false. OCaml reports that as a type error mentioning unit and F# says the branches have different types, but it is the same rule.Loops
The
done keyword disappears with the offside rule, and the counter no longer needs a ref cell.let () =
for index = 1 to 3 do
Printf.printf "for %d\n" index
done;
let countdown = ref 3 in
while !countdown > 0 do
Printf.printf "while %d\n" !countdown;
countdown := !countdown - 1
donefor index in 1 .. 3 do
printfn "for %d" index
let mutable countdown = 3
while countdown > 0 do
printfn "while %d" countdown
countdown <- countdown - 1OCaml's
for … to … do … done becomes F#'s for … in range do, where the range is an ordinary value — 1 .. 3 builds a sequence, and the same syntax works over a list, an array or anything enumerable. That makes F#'s for closer to a foreach than to OCaml's counting loop; the counting form for index = 1 to 3 do also exists for compatibility. As in the mutation row, the while counter is a real mutable binding in F# and a heap-allocated ref in OCaml.Tail Recursion
Both columns recurse a hundred thousand deep in constant stack space and print the same total. Two things had to be true for that, and only one of them is about recursion.
let rec sum_to total current =
if current = 0 then total
else sum_to (total + current) (current - 1)
let () = Printf.printf "%d\n" (sum_to 0 100000)// int64 here, not int — see the note below.
let rec sumTo (total: int64) (current: int64) =
if current = 0L then total
else sumTo (total + current) (current - 1L)
printfn "%d" (sumTo 0L 100000L)OCaml guarantees tail-call elimination as a language property. F# has no such guarantee from the runtime, but the compiler delivers it in the case that matters most: a directly self-recursive tail call is compiled into a loop, so this function never grows the stack. Tail calls to other functions — mutual recursion, or a call through a function value — are emitted with .NET's
.tail IL prefix, which the runtime honors at a noticeably higher cost per call and which is not emitted at all in every configuration. So the habit transfers for self-recursion and needs checking for anything mutual. The int64 annotations are the second thing: this total is 5,000,050,000, which does not fit in F#'s 32-bit default int and would silently print 705082704 instead. It fits in an OCaml int without comment. That is the integer-width row biting in real code, and it is worth seeing once.Collections
Lists
The list type, its literal syntax, its cons operator and its module functions are all the same. This is F# at its most OCaml.
let () =
let numbers = [ 1; 2; 3 ] in
let extended = 0 :: numbers in
List.iter (fun number -> Printf.printf "%d " number) extended;
print_newline ();
Printf.printf "length = %d\n" (List.length extended)let numbers = [ 1; 2; 3 ]
let extended = 0 :: numbers
for number in extended do
printf "%d " number
printfn ""
printfn "length = %d" (List.length extended)Both are immutable singly linked lists, both cons with
:: in constant time, both spell the literal with semicolons rather than commas — a detail that trips up anyone coming from almost any other language, and one an OCaml programmer never has to think about. The module functions match too: List.map, List.filter, List.length, List.rev. F# adds a range literal, [ 1 .. 10 ], and list comprehensions in the sequence-expression syntax, neither of which OCaml has.Arrays
Array literals use the same
[| … |] brackets. Only the indexing syntax changes, and it is a single character.let () =
let scores = [| 10; 20; 30 |] in
scores.(1) <- 99;
Array.iter (fun score -> Printf.printf "%d " score) scores;
print_newline ()let scores = [| 10; 20; 30 |]
scores.[1] <- 99
for score in scores do
printf "%d " score
printfn ""OCaml indexes an array with
array.(index) and a string with string.[index] — two different bracket styles for two different types. F# uses .[index] for both, and modern F# also accepts plain array[index]. Both languages leave arrays mutable regardless of how the binding was made, which is why neither column needs a mutable keyword to assign into one. An F# array is a real .NET int[], so it can be handed to any .NET method that wants one.The Persistent Map
Both maps are persistent and share structure. The difference is that F# needs no functor application to get one.
module StringMap = Map.Make (String)
let () =
let first = StringMap.empty in
let second = StringMap.add "language" "OCaml" first in
let third = StringMap.add "kind" "functional" second in
Printf.printf "second has %d\n" (StringMap.cardinal second);
Printf.printf "third has %d\n" (StringMap.cardinal third)let second = Map.ofList [ "language", "F#" ]
let third = second |> Map.add "kind" "functional"
printfn "second has %d" second.Count
printfn "third has %d" third.CountOCaml's
Map is a functor: Map.Make (String) builds a module specialized to string keys, and every operation is called through that module. F#'s Map<'Key, 'Value> is an ordinary generic type whose key constraint (comparison) the compiler checks directly, so there is nothing to instantiate and nothing to name. Note the F# tuple syntax in Map.ofList: a comma builds a tuple, where OCaml would need explicit parentheses. This is the first appearance of the functor gap, which has its own section below.Lazy Sequences
Both have a lazy sequence type with a matching module, and both are the answer to building a pipeline without intermediate lists.
let () =
let naturals = Seq.ints 1 in
let squares = Seq.map (fun number -> number * number) naturals in
Seq.iter (Printf.printf "%d ") (Seq.take 5 squares);
print_newline ()let squares = Seq.initInfinite (fun index -> (index + 1) * (index + 1))
for square in Seq.take 5 squares do
printf "%d " square
printfn ""OCaml's
Seq.t is a function returning a node on demand. F#'s seq<'T> is .NET's IEnumerable<'T>, so it interoperates with every .NET collection and every LINQ method. The module functions line up (map, filter, take, fold), and F# adds sequence expressions — seq { for x in 1 .. 5 -> x * x } — which are a computation expression and get their own section. The practical difference is that in F# a seq can be consumed only once, because IEnumerable is stateful once enumerated, where OCaml's Seq is re-runnable.Sets
Both sets are persistent and ordered. Once again the difference is the functor application OCaml needs and F# does not.
module IntSet = Set.Make (Int)
let () =
let first = IntSet.of_list [ 1; 2; 3 ] in
let second = IntSet.of_list [ 3; 4 ] in
Printf.printf "union has %d\n" (IntSet.cardinal (IntSet.union first second));
Printf.printf "has 3? %b\n" (IntSet.mem 3 first)let first = Set.ofList [ 1; 2; 3 ]
let second = Set.ofList [ 3; 4 ]
printfn "union has %d" (Set.union first second).Count
printfn "has 3? %b" (first.Contains 3)OCaml's
Set.Make (Int) builds a module specialized to integer elements, and every operation goes through it. F#'s Set<'T> is a generic type whose comparison constraint the compiler checks directly. Both are immutable balanced trees with structural sharing, so union is cheap in both and neither copies. F# exposes the operations twice over — as module functions (Set.union, Set.contains) for pipelining and as members (.Count, .Contains) for dot notation — and this row uses one of each to show they are the same thing.Ranges and Comprehensions
F# has range literals and list comprehensions; OCaml has neither, and builds the same list with combinators.
(* No range or comprehension syntax; build with a
combinator and filter afterwards. *)
let () =
let evens =
List.filter (fun number -> number mod 2 = 0) (List.init 10 (fun index -> index + 1))
in
List.iter (Printf.printf "%d ") evens;
print_newline ()let evens = [ for number in 1 .. 10 do
if number % 2 = 0 then
yield number ]
for number in evens do
printf "%d " number
printfn ""The range
1 .. 10 is a value in F#, usable anywhere a sequence is, and it accepts a step (1 .. 2 .. 10). The comprehension is the same computation-expression machinery as seq { }, just collected into a list by the surrounding brackets, so a for, an if and multiple yields per iteration are all available inside it. OCaml has List.init to build a list of known length and no comprehension syntax at all — a gap that shows up whenever the construction logic is more than one combinator deep.Functions & Currying
Currying and Partial Application
Currying survives intact. Every F# function of several arguments is a function returning a function, exactly as in OCaml.
let add first second = first + second
let () =
let add_ten = add 10 in
Printf.printf "%d\n" (add_ten 5);
Printf.printf "%d\n" (add_ten 32)let add first second = first + second
let addTen = add 10
printfn "%d" (addTen 5)
printfn "%d" (addTen 32)This is the feature whose absence hurts most when moving to almost any other language, and F# keeps it.
add has type int -> int -> int in both columns, and applying it to one argument is ordinary application rather than a special case. The one place F# breaks the rule is when calling a .NET method: text.Split(',') takes a tuple of arguments, not a curried sequence, so .Split cannot be partially applied. That distinction between F# functions and .NET methods is worth internalizing early.The Pipeline Operator
F# took
|> from OCaml, and it is far more central to F# style than it ever became in OCaml.let () =
[ 1; 2; 3; 4; 5 ]
|> List.filter (fun number -> number mod 2 = 1)
|> List.map (fun number -> number * number)
|> List.fold_left ( + ) 0
|> Printf.printf "%d\n"[ 1; 2; 3; 4; 5 ]
|> List.filter (fun number -> number % 2 = 1)
|> List.map (fun number -> number * number)
|> List.fold ( + ) 0
|> printfn "%d"The operator is the same one-line definition in both (
let (|>) value function = function value) and works for the same reason: the module functions are curried and take their data last. F# leans on it much harder — a typical F# program is written almost entirely as pipelines, and the standard library is designed for it. F# also has >> and << for function composition, which OCaml leaves to the user to define.Labeled and Optional Arguments
A real loss. OCaml's labeled and optional arguments are a language feature; F# offers nothing equivalent for plain functions.
let greet ?(greeting = "Hello") ~name () =
Printf.printf "%s, %s!\n" greeting name
let () =
greet ~name:"Ada" ();
greet ~greeting:"Welcome" ~name:"Alan" ()// F# functions have neither. Optional arguments exist only
// on METHODS of a type, so a plain function takes a default.
let greet (greeting: string) (name: string) =
printfn "%s, %s!" greeting name
greet "Hello" "Ada"
greet "Welcome" "Alan"F# does have optional parameters, written
?greeting, but only on members of a class or record — never on a module-level let function. So the OCaml habit of giving a function a defaulted, named parameter has no direct translation, and the F# answers are to define a type with a method, to pass a record of options, or simply to require the argument. The trailing () in the OCaml column is the usual marker that a function with optional arguments has been fully applied; F# needs no such thing because it has nothing to disambiguate.A Numerically Generic Function
F# has a mechanism for this that OCaml genuinely lacks, and it is the compensation for losing the separate float operators.
(* OCaml cannot write one function over int AND float:
the operators are different, so there are two. *)
let double_int value = value * 2
let double_float value = value *. 2.0
let () =
Printf.printf "%d\n" (double_int 21);
Printf.printf "%.1f\n" (double_float 21.0)// inline plus a statically resolved type parameter gives one
// function that works for every numeric type.
let inline double value = value + value
printfn "%d" (double 21)
printfn "%.1f" (double 21.0)Marking a function
inline lets F# resolve its operators at each call site rather than committing to one type, using what the language calls statically resolved type parameters. The result is a single double that works for int, float, decimal and any type with a suitable +. OCaml has no equivalent short of passing the operations explicitly — which is precisely what a functor does, and is why OCaml reaches for one where F# reaches for inline. The constraint is inferred here; when it needs writing out the syntax is notoriously dense.Records
Defining a Record
Records are nearly identical, including the
with-update syntax that most languages spell some other way.type point = { x : int; y : int }
let () =
let origin = { x = 0; y = 0 } in
let shifted = { origin with x = 5 } in
Printf.printf "(%d, %d)\n" origin.x origin.y;
Printf.printf "(%d, %d)\n" shifted.x shifted.ytype Point = { X: int; Y: int }
let origin = { X = 0; Y = 0 }
let shifted = { origin with X = 5 }
printfn "(%d, %d)" origin.X origin.Y
printfn "(%d, %d)" shifted.X shifted.YThe only differences are conventions: F# capitalizes type and field names, and its type annotation uses
Name: type where OCaml writes name : type. Both are immutable by default and both support functional update with with. Underneath, an F# record is a sealed .NET class, which is what gives it the structural equality and comparison discussed in the next row — an OCaml record is a heap block with no methods at all.Structural Equality
Both print
true twice, and the mechanism underneath is completely different in a way that matters.type point = { x : int; y : int }
(* OCaml's = is POLYMORPHIC: it inspects representations at
runtime and works on any type, including functions — where
it raises. *)
let () =
Printf.printf "%b\n" ({ x = 1; y = 2 } = { x = 1; y = 2 });
Printf.printf "%b\n" ([ 1; 2 ] = [ 1; 2 ])type Point = { X: int; Y: int }
// F#'s = is resolved by the type's equality CONTRACT, which
// records and unions get automatically and functions do not.
printfn "%b" ({ X = 1; Y = 2 } = { X = 1; Y = 2 })
printfn "%b" ([ 1; 2 ] = [ 1; 2 ])OCaml's
= has type 'a -> 'a -> bool and works by walking the runtime representation, so it compares anything — and raises Invalid_argument at runtime if it meets a function or a cyclic value. F# derives a real structural equality implementation for each record and union at compile time, so = is checked: comparing two functions is a compile error, not a runtime one. The trade is that an F# type can opt out ([<ReferenceEquality>]) or need [<CustomEquality>], whereas in OCaml every type gets the same behavior whether it suits it or not.A Mutable Field
The
mutable field keyword and the <- assignment operator are both the same.type counter = { mutable total : int }
let () =
let counter = { total = 0 } in
counter.total <- counter.total + 5;
counter.total <- counter.total + 5;
Printf.printf "%d\n" counter.totaltype Counter = { mutable Total: int }
let counter = { Total = 0 }
counter.Total <- counter.Total + 5
counter.Total <- counter.Total + 5
printfn "%d" counter.TotalThis is another feature F# took verbatim. In both languages the mutability is a property of the field, declared once, and applies to every holder of the record. One F#-only consequence: a record containing a mutable field can no longer be used as a key in a
Map or Set without care, because its structural hash would change under it — OCaml's Map has the same hazard but reports it as a mysteriously missing key rather than as anything explicit.Unions & Pattern Matching
Variants and Discriminated Unions
Sum types are the same feature with the same syntax; only the float operators and the layout differ.
type shape =
| Circle of float
| Rectangle of float * float
| Point
let area = function
| Circle radius -> 3.14159 *. radius *. radius
| Rectangle (width, height) -> width *. height
| Point -> 0.0
let () =
List.iter
(fun shape -> Printf.printf "%.2f\n" (area shape))
[ Circle 1.0; Rectangle (2.0, 3.0); Point ]type Shape =
| Circle of float
| Rectangle of float * float
| Point
let area shape =
match shape with
| Circle radius -> 3.14159 * radius * radius
| Rectangle (width, height) -> width * height
| Point -> 0.0
for shape in [ Circle 1.0; Rectangle (2.0, 3.0); Point ] do
printfn "%.2f" (area shape)Both check exhaustiveness, both report a missing case as a warning or error naming the constructor, and both allow the leading
| on the first case. F# also has the function keyword as shorthand for fun x -> match x with, exactly as OCaml does, so the anchor column's spelling would compile in F# too — it is written out here because the explicit match is the more common F# style. F# additionally allows named union fields (Circle of radius: float), which OCaml gained only recently and spells differently.Option
Same type, same constructors, same idea. The only thing to learn is the naming convention for the functions that return one.
let () =
(match List.find_opt (fun number -> number mod 2 = 0) [ 1; 3; 4; 5 ] with
| Some number -> Printf.printf "found %d\n" number
| None -> print_endline "none found");
(match List.find_opt (fun number -> number mod 2 = 0) [ 1; 3; 5 ] with
| Some number -> Printf.printf "found %d\n" number
| None -> print_endline "none found")let describe numbers =
match List.tryFind (fun number -> number % 2 = 0) numbers with
| Some number -> printfn "found %d" number
| None -> printfn "none found"
describe [ 1; 3; 4; 5 ]
describe [ 1; 3; 5 ]OCaml suffixes the option-returning variant with
_opt (find_opt, nth_opt, assoc_opt); F# prefixes it with try (tryFind, tryItem, tryHead). Both libraries also keep the raising version under the bare name. The combinators correspond too: Option.map is Option.map, Option.value ~default is Option.defaultValue, and Option.bind is Option.bind.Guards and Or-Patterns
Guards, or-patterns and the wildcard are spelled identically,
when keyword included.let classify number =
match number with
| 0 -> "zero"
| n when n < 0 -> "negative"
| 1 | 2 | 3 -> "small"
| _ -> "ordinary"
let () =
List.iter
(fun number -> Printf.printf "%d is %s\n" number (classify number))
[ 0; -5; 2; 42 ]let classify number =
match number with
| 0 -> "zero"
| n when n < 0 -> "negative"
| 1 | 2 | 3 -> "small"
| _ -> "ordinary"
for number in [ 0; -5; 2; 42 ] do
printfn "%d is %s" number (classify number)F# kept OCaml's
when rather than adopting a different keyword, and the exhaustiveness rules match: a guard does not count toward coverage in either language, so the wildcard is required in both columns. The one addition F# makes is that a pattern may be bound with as in both languages, and F# extends matching further through active patterns — the next section, and the biggest thing this page has to teach.No Polymorphic Variants
The first of two OCaml features F# left behind, and the one an OCaml programmer is most likely to reach for by reflex.
(* A polymorphic variant needs no declaration and can
belong to several types at once. *)
let describe value =
match value with
| `Circle radius -> Printf.sprintf "circle of %d" radius
| `Square side -> Printf.sprintf "square of %d" side
let () =
print_endline (describe (`Circle 3));
print_endline (describe (`Square 4))// F# requires the union to be declared, like every other
// language that is not OCaml.
type Shape =
| Circle of int
| Square of int
let describe value =
match value with
| Circle radius -> sprintf "circle of %d" radius
| Square side -> sprintf "square of %d" side
printfn "%s" (describe (Circle 3))
printfn "%s" (describe (Square 4))A polymorphic variant tag is not tied to any one type: two unrelated functions can accept overlapping sets of tags, and the inferred type records exactly which tags a value might carry, which makes open extensible sums possible with no declaration in advance. F# has nothing like it — every case belongs to one declared union. What F# offers instead is not a replacement but a different tool: active patterns let you match flexibly over existing types without declaring anything, which covers some of the same ergonomic ground from the consuming side rather than the producing side.
Active Patterns
A Complete Active Pattern
This is the largest feature F# has that OCaml does not, and it changes what pattern matching is for.
(* OCaml matches on the SHAPE of a value. To classify by a
computed property, you compute first and match after. *)
let () =
let classify number = if number mod 2 = 0 then "even" else "odd" in
List.iter
(fun number -> Printf.printf "%d is %s\n" number (classify number))
[ 1; 2; 3; 4 ]// An active pattern turns a computation into a pattern, so
// the match itself can ask "is this even?"
let (|Even|Odd|) number =
if number % 2 = 0 then Even else Odd
for number in [ 1; 2; 3; 4 ] do
match number with
| Even -> printfn "%d is even" number
| Odd -> printfn "%d is odd" numberIn OCaml,
match can only take apart a value along the lines its type declaration already drew. An active pattern lets you define new lines: (|Even|Odd|) is an ordinary function whose name makes it usable as a pattern, so Even and Odd become matchable cases on a plain int. Crucially the compiler still checks exhaustiveness — deleting the Odd branch produces an incomplete-match warning, because the pattern declares that those two cases are all there are. That combination of a user-defined view with a preserved exhaustiveness guarantee has no OCaml counterpart.A Partial Active Pattern
A partial active pattern may fail to match, which is how a parse becomes a pattern rather than a step before one.
let () =
let describe text =
match int_of_string_opt text with
| Some number -> Printf.sprintf "the number %d" number
| None -> Printf.sprintf "the text %S" text
in
print_endline (describe "42");
print_endline (describe "hello")let (|Number|_|) (text: string) =
match System.Int32.TryParse text with
| true, value -> Some value
| false, _ -> None
let describe text =
match text with
| Number value -> sprintf "the number %d" value
| other -> sprintf "the text %A" other
printfn "%s" (describe "42")
printfn "%s" (describe "hello")The
_| in (|Number|_|) marks the pattern as partial: it returns an option, and a None simply means this case does not apply. The payoff is that parsing, regular-expression matching and type tests all become things you write inside a match rather than before it, so a chain of alternatives reads as one construct. The OCaml column has to bind the parse result first and match on the option, which works but forces a nesting level per test. Note the F# idiom for .NET's out-parameter methods: Int32.TryParse returns a tuple of the boolean and the value.A Pattern That Decomposes
An active pattern can also produce several values, so a decomposition happens in the pattern position instead of before it.
(* Getting at both halves of a value means two calls and
a tuple, or a helper that returns one. *)
let () =
let split_name full =
match String.index_opt full ' ' with
| Some position ->
(String.sub full 0 position,
String.sub full (position + 1) (String.length full - position - 1))
| None -> (full, "")
in
let (first, last) = split_name "Ada Lovelace" in
Printf.printf "%s / %s\n" first lastlet (|SplitName|) (full: string) =
match full.IndexOf(' ') with
| -1 -> (full, "")
| position -> (full.Substring(0, position), full.Substring(position + 1))
match "Ada Lovelace" with
| SplitName (first, last) -> printfn "%s / %s" first lastThis is the form that most changes how code reads. A single-case active pattern always succeeds and is used purely as a view:
SplitName (first, last) both destructures and names, in the place where an OCaml programmer would have called a helper and then destructured its tuple. Nested inside a larger match, several such views compose into one pattern. There is no OCaml analogue at all — OCaml's patterns are strictly structural, which is simpler to reason about and strictly less expressive.Modules & the Missing Functor
Modules
A plain module is a plain module in both. The difference appears the moment you want to abstract over one.
module Geometry = struct
let pi = 3.14159
let circle_area radius = pi *. radius *. radius
end
let () =
Printf.printf "%.4f\n" Geometry.pi;
Printf.printf "%.2f\n" (Geometry.circle_area 2.0)module Geometry =
let pi = 3.14159
let circleArea radius = pi * radius * radius
printfn "%.4f" Geometry.pi
printfn "%.2f" (Geometry.circleArea 2.0)F# drops the
struct … end delimiters in favor of indentation, and that is the whole visible difference here. Underneath, an F# module compiles to a static .NET class and its members to static methods, which is what makes them callable from C#. The consequence is that an F# module is a compile-time namespace and nothing more: it cannot be passed as a value, returned, or stored — none of which is true of an OCaml module, and all of which the next two rows depend on.The Functor Has No Translation
The second thing F# left behind, and the one that most changes how a large program is organized.
module type Comparable = sig
type t
val compare : t -> t -> int
end
module MakeLargest (Element : Comparable) = struct
let largest items =
List.fold_left
(fun best item -> if Element.compare item best > 0 then item else best)
(List.hd items) (List.tl items)
end
module IntCompare = struct
type t = int
let compare = compare
end
module LargestInt = MakeLargest (IntCompare)
let () = Printf.printf "%d\n" (LargestInt.largest [ 3; 9; 4 ])// F# has no functors. A generic function with a constraint
// covers the common case with far less ceremony.
let largest (items: 'T list when 'T: comparison) =
List.reduce (fun best item -> if item > best then item else best) items
printfn "%d" (largest [ 3; 9; 4 ])An OCaml functor is a function from modules to modules, so parameterizing over "a type plus operations on it" means building and naming a new module. F# has no such construct at any level: the substitutes are generic types with constraints (as here), interfaces passed as values, and
inline with statically resolved type parameters. For the common case — one type parameter with a standard constraint — the F# version is dramatically shorter. For the cases functors exist to serve, such as parameterizing a whole data structure over several types and operations at once, or instantiating the same functor twice with different behavior for one type, F# has no good answer and you restructure around it.A Signature vs an Interface
Where OCaml packs a module into a value, F# reaches for a .NET interface and an object expression.
module type Greeter = sig val greet : unit -> string end
module English = struct let greet () = "Hello" end
module French = struct let greet () = "Bonjour" end
let () =
let greeters : (module Greeter) list =
[ (module English); (module French) ]
in
List.iter
(fun greeter ->
let module G = (val greeter : Greeter) in
print_endline (G.greet ()))
greeterstype IGreeter =
abstract member Greet: unit -> string
let english = { new IGreeter with member _.Greet() = "Hello" }
let french = { new IGreeter with member _.Greet() = "Bonjour" }
for greeter in [ english; french ] do
printfn "%s" (greeter.Greet())OCaml's first-class modules let a module be packed with
(module English) and unpacked with (val …), which is the general mechanism — a packed module can carry types as well as values. F#'s answer is the interface, and { new IGreeter with … } is an object expression, an anonymous implementation created inline with no class declaration. It is lighter than OCaml's packing syntax for this case, and strictly less general: an interface can carry members but not an abstract type, so the OCaml signature's type t has no counterpart here.Objects, Interfaces & null
null Arrives Through the Back Door
The single biggest hazard in the whole language for someone arriving from OCaml, and it is not visible in any type signature.
(* OCaml has no null. An absent value is an option, and
the type system knows it. *)
let () =
let lookup key = if key = "known" then Some "value" else None in
(match lookup "known" with
| Some found -> print_endline found
| None -> print_endline "absent");
(match lookup "other" with
| Some found -> print_endline found
| None -> print_endline "absent")// F# types cannot be null — but .NET types CAN, and a string
// from any .NET API may be null with nothing in its type saying so.
let lookup (key: string) : string =
if key = "known" then "value" else null
let describe key =
match lookup key with
| null -> printfn "absent"
| found -> printfn "%s" found
describe "known"
describe "other"Types you declare in F# — records, unions, tuples — cannot be
null, and the compiler rejects the literal. But every type that comes from .NET can be, and System.String is one of them, so a string returned by any library method may be null with nothing in its type to warn you. The habit to build is to convert at the boundary: Option.ofObj turns a possibly-null reference into an option the moment it enters your code, after which the rest of the program can be written the way you would write OCaml. Match on null only at that boundary, as this row does.Classes
Both languages have an object system. The difference is whether anyone uses it.
(* OCaml has an object system, and almost nobody uses it.
The idiomatic spelling is a record plus functions. *)
type account = { owner : string; mutable balance : int }
let deposit account amount = account.balance <- account.balance + amount
let () =
let account = { owner = "Ada"; balance = 0 } in
deposit account 50;
Printf.printf "%s has %d\n" account.owner account.balance// F# has real .NET classes, and they are how you expose an
// API that C# or VB callers will consume.
type Account(owner: string) =
let mutable balance = 0
member _.Owner = owner
member _.Balance = balance
member _.Deposit(amount) = balance <- balance + amount
let account = Account("Ada")
account.Deposit(50)
printfn "%s has %d" account.Owner account.BalanceOCaml's objects are structurally typed and genuinely interesting, and they are also almost absent from real OCaml code — records and modules cover the ground. F# classes are ordinary .NET classes with nominal typing, inheritance and interfaces, and they are unavoidable: every library you call is built from them, and any F# code meant to be consumed from C# has to expose them. The syntax above is a primary constructor with the parameters in the type name, which has no OCaml analogue. For code that only F# will call, records and unions remain the idiomatic choice.
Reaching the Standard Library
Most of what an F# program calls is the .NET base class library, which is far larger than OCaml's standard library and shaped completely differently.
let () =
let text = " padded " in
print_endline (String.trim text);
Printf.printf "%d\n" (int_of_string "42");
Printf.printf "%.4f\n" (sqrt 2.0)let text = " padded "
printfn "%s" (text.Trim())
printfn "%d" (System.Int32.Parse "42")
printfn "%.4f" (sqrt 2.0)This is the practical upside of the .NET foundation. OCaml's standard library is deliberately small — no HTTP client, no JSON, no date handling beyond
Unix, so almost anything needs an opam package. F# inherits everything .NET has, so networking, cryptography, dates, culture-aware formatting and regular expressions are all present without a dependency. The style clash is real: those APIs are object-oriented and uncurried, so they do not pipeline, and idiomatic F# tends to wrap the ones it uses often in small curried functions.Converting Between Types
Neither language widens a number for you, which is unusual on .NET and is one of the places F# broke with its host platform on purpose.
(* OCaml names every conversion explicitly, and there is
no implicit widening anywhere. *)
let () =
let count = 7 in
let average = float_of_int count /. 2.0 in
Printf.printf "%.1f\n" average;
Printf.printf "%d\n" (int_of_float average);
Printf.printf "%s\n" (string_of_int count)// F# also refuses implicit numeric conversion — the
// conversion functions are named for their target type.
let count = 7
let average = float count / 2.0
printfn "%.1f" average
printfn "%d" (int average)
printfn "%s" (string count)C# will silently widen an
int to a double; F# will not, and neither will OCaml. What changes is the naming: OCaml spells out both ends (float_of_int, int_of_float, string_of_int) while F# names only the destination (float, int, string), overloading each across every sensible source type. F#'s int truncates toward zero exactly as OCaml's int_of_float does. The one asymmetry worth remembering is that F#'s string works on any type by calling ToString(), so it will happily produce something unhelpful rather than failing to compile.Error Handling
Exceptions
Custom exceptions are declared and raised with the same keywords; only the layout of
try … with changes.exception Too_large of int
let check value =
if value > 100 then raise (Too_large value) else value
let () =
Printf.printf "%d\n" (check 50);
(try Printf.printf "%d\n" (check 500) with
| Too_large value -> Printf.printf "too large: %d\n" value)exception TooLarge of int
let check value =
if value > 100 then raise (TooLarge value) else value
printfn "%d" (check 50)
try
printfn "%d" (check 500)
with
| TooLarge value -> printfn "too large: %d" valueF# kept OCaml's
exception declaration and its raise, and try … with pattern-matches the exception exactly as OCaml does. Two additions come from .NET: try … finally, which OCaml lacks entirely (its equivalent is the Fun.protect function), and the fact that an F# exception is a real System.Exception subclass, so it can be caught by C# callers and carries a stack trace. Both languages treat exceptions as ordinary and idiomatic, unlike Rust or Go.Result
The
result type and its two constructors are the same in both, and so is the awkwardness of chaining several of them.let parse_positive text =
match int_of_string_opt text with
| None -> Error (Printf.sprintf "%S is not a number" text)
| Some number when number <= 0 -> Error "must be positive"
| Some number -> Ok number
let () =
(match parse_positive "42" with
| Ok number -> Printf.printf "%d\n" number
| Error message -> print_endline message);
(match parse_positive "oops" with
| Ok number -> Printf.printf "%d\n" number
| Error message -> print_endline message)let parsePositive (text: string) =
match System.Int32.TryParse text with
| false, _ -> Error (sprintf "%A is not a number" text)
| true, number when number <= 0 -> Error "must be positive"
| true, number -> Ok number
let describe text =
match parsePositive text with
| Ok number -> printfn "%d" number
| Error message -> printfn "%s" message
describe "42"
describe "oops"OCaml spells the type
('a, 'b) result and F# spells it Result<'T, 'Error>, but Ok and Error are identical. Neither language has Rust's ? operator, so chaining means Result.bind or a custom binding operator in OCaml, and Result.bind or a result computation expression in F# — the latter being much the more pleasant, and the subject of the next section.Computation Expressions
let* vs a Computation Expression
Both languages solve the same problem — flattening a chain of binds — and F#'s solution is considerably more general.
(* OCaml 4.08+ has binding operators: define let* once
and the nesting flattens. *)
let ( let* ) = Option.bind
let total first second =
let* left = int_of_string_opt first in
let* right = int_of_string_opt second in
Some (left + right)
let () =
(match total "3" "4" with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "not both numbers");
(match total "3" "oops" with
| Some value -> Printf.printf "%d\n" value
| None -> print_endline "not both numbers")// F#'s computation expression is the same idea, generalized:
// a builder object defines what let!, return and more mean.
type OptionBuilder() =
member _.Bind(value, binder) = Option.bind binder value
member _.Return(value) = Some value
let option = OptionBuilder()
let total (first: string) (second: string) =
option {
let! left = (match System.Int32.TryParse first with
| true, value -> Some value
| false, _ -> None)
let! right = (match System.Int32.TryParse second with
| true, value -> Some value
| false, _ -> None)
return left + right
}
let describe first second =
match total first second with
| Some value -> printfn "%d" value
| None -> printfn "not both numbers"
describe "3" "4"
describe "3" "oops"OCaml's binding operators let you define
let*, and* and let+ as ordinary infix definitions, which flattens monadic code neatly and is entirely sufficient for Option and Result. F#'s computation expression is a whole protocol: a builder type may define Bind, Return, ReturnFrom, For, While, TryWith, Use and more, so the block can contain loops, exception handling and resource cleanup that all thread through the same effect. That is why seq, async, task and query are all the same feature. The cost is visible above: the builder must be written before anything can use it.Sequence Expressions
The built-in
seq { } expression is the computation expression an F# programmer meets first, usually without realizing what it is.(* Building a sequence means composing Seq combinators. *)
let () =
let squares =
Seq.filter (fun number -> number mod 2 = 1) (Seq.take 10 (Seq.ints 1))
|> Seq.map (fun number -> number * number)
in
Seq.iter (Printf.printf "%d ") squares;
print_newline ()// A sequence expression reads as a loop and builds a lazy seq.
let squares =
seq { for number in 1 .. 10 do
if number % 2 = 1 then
yield number * number }
for square in squares do
printf "%d " square
printfn ""The two columns produce the same numbers by completely different routes. OCaml composes combinators, which is concise for simple pipelines and turns awkward once conditionals and nested loops are involved. The F# version reads as ordinary imperative code — a
for, an if, a yield — and produces a lazy sequence, because seq is a builder whose For and Yield members thread everything together. Nested loops and multiple yields per iteration stay readable in the F# form and become genuinely hard in the OCaml one.Async & Concurrency
Asynchronous Work
The two languages arrived at concurrency from opposite ends, and the difference is where the machinery lives.
(* OCaml 5 has effects and domains; asynchronous I/O comes
from a library (Eio or Lwt), not the language. Domains
are the standard-library primitive. *)
let () =
let worker = Domain.spawn (fun () ->
let total = ref 0 in
for index = 1 to 100 do total := !total + index done;
!total)
in
Printf.printf "%d\n" (Domain.join worker)// async is a computation expression, in the standard library.
let work = async {
let mutable total = 0
for index in 1 .. 100 do
total <- total + index
return total
}
printfn "%d" (Async.RunSynchronously work)F#'s
async is not a keyword but a computation expression built on the same protocol as seq, which is why let!, for and try all work inside it. It composes: Async.Parallel takes a list of them and runs them together. OCaml 5 instead exposes Domain for parallelism and effect handlers for concurrency, and leaves the scheduler to libraries such as Eio. The consequence an OCaml programmer will feel is that F# has "colored" functions — an Async<'T> is a different type that must be run — while OCaml 5's effects deliberately avoid that, letting ordinary-looking code suspend.Running Work in Parallel
Fanning work out and collecting it back reads similarly, and the units of work are different things underneath.
let () =
let square value () = value * value in
let workers = List.map (fun value -> Domain.spawn (square value)) [ 1; 2; 3; 4 ] in
let results = List.map Domain.join workers in
List.iter (Printf.printf "%d ") results;
print_newline ()let square value = async { return value * value }
let results =
[ 1; 2; 3; 4 ]
|> List.map square
|> Async.Parallel
|> Async.RunSynchronously
for result in results do
printf "%d " result
printfn ""Each OCaml
Domain.spawn creates an operating-system thread with its own minor heap — they are expensive, and the guidance is to create about as many as you have cores. Each F# async is a value describing work, and Async.Parallel hands them to the .NET thread pool, so creating thousands is ordinary. That difference decides how you structure a program: OCaml pairs a small number of domains with a library-level scheduler for the fine-grained concurrency, while F# lets the runtime's pool handle it.Units of Measure
Units of Measure
F# can put physical units into the type system, check them at compile time, and erase them entirely at runtime. Nothing in OCaml comes close.
(* Both values are just float, and so is their quotient.
Nothing here stops you adding a distance to a time. *)
let distance = 100.0
let time = 9.58
let speed = distance /. time
let () = Printf.printf "%.2f\n" speed[<Measure>] type m
[<Measure>] type s
let distance = 100.0<m>
let time = 9.58<s>
let speed = distance / time
printfn "%.2f" (float speed)Both columns do the same arithmetic and print the same number, and only one of them checked it. A measure annotation makes
100.0<m> a different type from 9.58<s>, and dividing them produces float<m/s> — the compiler does the algebra on the units, so adding meters to seconds is a type error and assigning a speed to a distance is too. All of it is erased at compile time, so this is ordinary float arithmetic at runtime with no cost at all. OCaml's nearest approach is an abstract type per unit hidden behind a module signature, which does stop you mixing meters with seconds — but it cannot express a derived unit, so the quotient above has nowhere to live and you are back to bare floats exactly where the checking would have mattered most. Units of measure are the clearest case of F# saying something in the type system that OCaml's type system, for all its power, cannot.