Comparing OCAML to F#

jkone27
14 min readFeb 22, 2024

Same thing or not? is it based?

Camel or Dromedary, what’s the difference between F# and Ocaml? *note: these images are not related to logos or branding, but more like their essence

ML Family 👨‍👩‍👦 💜

F# was conceived as an OCaml extension for the .NET platform, so in that sense, they still share a lot regarding syntax and type systems.

For example, both take advantage of great automatic generalized type inference and Hindley Milner derivatives of inference algorithms.

Regarding language features, while many features are the same or shared/inherited between the two, still some features differ because of their respective divergent evolutions.

In addition to this, we have a few features that are also unique to a specific language and haven’t made it to the counterpart, because of language design choices from their respective maintainers/creators.

But know this, from a chronological/genealogical point of view F# is the child of OCaml, see the diagram below.

we can say F# is a child of Ocaml, in the genealogy tree sense

They are both part of the ML language family and with this family, they share a lot of features as well.

To repeat, being so closely related together their likeness is greater than we would imagine.

Ocaml splash page and description

For this reason, a relatively great deal of OCaml code is swappable with F# code and vice-versa, but not all of it!

Most simple code examples and purely functional code in modules are (except generic modules, see later) the same in Ocaml and F#, the code is identical, like to like, as water droplets.

F# dotnet splash page

Simple Example

try OCaml example online here

in F# becomes:

try F# example online here

OCaml and F# behave a bit differently in regards to bindings with let, you can see in general OCaml is a bit stricter with the in keyword in bindings, while F# makes it optional (in practice never used in F# code).

F# makes it a bit easier also to evaluate multiple top-level statements, while in OCaml I had to use some tricks like binding a single expression to unit (), and chain multiple unit operations via the ; keyword.

I will try to list below the main differences between the 2 languages.

1. LET

For both OCaml and F# we can declare a value or a function binding with the same let keyword. the awesome Hindley-Milner type inference (or their language-specific implementation) will always give us inferred types for the likes of other members of the ML language family.

let x = 5 // int

let mySum a = a + 1 // int -> int

Both OCaml and F# use the let keyword to bind a value to a name. This creates an immutable binding in both languages, meaning that once a value is assigned to a name, it cannot be changed.

Mutability

In OCaml, to achieve mutability, one must use the ref type along with the := operator for assignment. For example:

let x = ref 5
x := 10

F#: F# offers a more straightforward approach with the mutable keyword modifier, but still supports ref cells if needed, for instance:

let mutable x = 5
x <- 10

While OCaml requires the use of ref and := for mutability, F# provides a more concise syntax using let mutable and the <- operator, a bit closer to Rust (which also uses mutable let modifier).

Adopting a custom operator for mutation, grants also a compiler error if trying to mutate a non-mutable variable, so this is in general a good choice.

The preference for clarity over the 2 approaches is debated*, but is not so important for the comparison of the 2, cc Yawar λmin thanks for your contribution.

Local Bindings

Ocaml

let result =
let x = 5 in
let y = 10 in
x + y

F#

let result =
let x = 5
let y = 10
x + y

The in keyword in OCaml is mandatory for declaring local let bindings, while in F#, it is optional and often omitted for brevity. This slight difference in syntax reflects the differing conventions and priorities between the two languages, with F# favouring terseness and OCaml emphasizing explicit scoping.

2. Comments

not so important but in OCaml comments are always defined as (**) whereas in F# both (**) and the more usual // syntax is supported.
for XML docs comment in F# is possible to use the /// notation, whereas in Ocaml for documentation it is possible to use the (** **) notation.

3. Signatures / Interfaces

In OCaml, interface module definitions allow for specifying signatures that define the types and functions a module must implement. These signatures are separate from the module implementations and are typically defined in .mli files. For example:

(* Module signature definition in .mli file *)
module type MyModuleSig = sig
type t
val func : t -> int
end


(* Module implementation in .ml file *)
module MyModule : MyModuleSig = struct
type t = int
let func x = x
end

In F# signature definitions are available as well, but are usually not adopted, if not for speeding up compilation in bigger projects — I am not much aware of other usage, add more information in the comments if you have any. In contrast, what is commonly used is fully abstract types which are also called interfaces in .NET (when prefixed with an I in the name by convention). These interfaces specify a contract that concrete types must adhere to, for example:

// Interface definition
type IMyInterface =
abstract member Func : int -> int

// Concrete type implementing the interface
type MyType() =
interface IMyInterface with
member this.Func x = x

While the concepts are similar, the implementation details differ.

In F# regular modules are also frequently used, not necessarily together with the signature file, as a primary way to organize code, and they appear as static classes with static methods if referenced from C# (and supposedly also if inspected in the compiled intermediate language).

module Test = 
let something = 5

module Test2 =
let something2 = 10

Interestingly in F# we also have a way to declare modules in a shortened way with the = only, whereas OCaml favours the = struct …. end construct.

Generic Modules vs Interfaces<T>

In OCaml, signature modules allow for specifying the interface of a module, while genericity is achieved through functors. In contrast, F# relies on interfaces and generics for similar functionality, in general, abstract signatures are more versatile, but also abstract interfaces usually allow for enough generalization for most use cases, this topic is probably debated. In other languages like Haskell, type classes provide yet another level of higher-level abstraction.

Here’s an example of a signature module and a functor in OCaml:

(* Signature module specifying the interface of a stack, .mli *)
module type StackSig = sig
type 'a t
val empty : 'a t
val push : 'a -> 'a t -> 'a t
val pop : 'a t -> 'a option * 'a t
end



(* Functor for creating a stack module with a specific implementation, .ml *)
module StackFunctor (E : sig type t end) : StackSig with type 'a t = E.t list = struct
type 'a t = E.t list
let empty = []
let push x s = x :: s
let pop = function [] -> (None, []) | x :: xs -> (Some x, xs)
end

In F#, there is no direct support for generic modules as in OCaml. Instead, F# typically relies on generic interfaces to achieve similar (yet not equal) functionality. Here’s an example of using a generic interface in F#

// Interface specifying the interface of a stack
type IStack<'a> =
abstract member Empty : 'a list
abstract member Push : 'a -> 'a list -> 'a list
abstract member Pop : 'a list -> 'a option * 'a list

// Generic class implementing the stack interface
type Stack<'a>() =
interface IStack<'a> with
member this.Empty = []
member this.Push x s = x :: s
member this.Pop = function [] -> (None, []) | x :: xs -> (Some x, xs)

4. Lists

let lst = [ 1 ; 2 ; 3 ]

in F# record constructor and lists in general do not require ; when expressed vertically (indentation), whereas in OCaml they do. This makes it possible in F# to build vertical list definitions in addition to horizontal ones, and for the composition of multiple list higher-order functions, allowing to elegantly represent markup domain-specific languages (DSL) very easily.

let lst = 
[
1
2
3
]

This fact allowed the development of many beautiful HTML DSLs in the F# ecosystem, e.g. Feliz and Giraffe/Fable-Elmish for both FE and BE.

5. Tooling

OPAM splash

OCaml

  • OCaml tooling is centred around the OPAM (OCaml Package Manager) ecosystem for package management and project setup.
  • Editors like Emacs, Vim, and VS Code provide support for OCaml development with extensions and plugins.
  • Dune (formerly known as JBuilder) is a modern build system for OCaml projects, offering efficient compilation and dependency management.
  • Merlin/ocaml-lsp provides editor support for OCaml with features like code completion, type inference, and error checking.
  • OCamlfind and ocamlbuild are commonly used for compiling and building OCaml projects, with support for finding and linking OCaml packages.
  • While not as extensive as the .NET ecosystem, OCaml tooling provides essential features for the effective development and maintenance of OCaml projects.

F# .NET tooling

  • F# benefits from strong tooling support within the .NET ecosystem, including Visual Studio, Vs for Mac and Rider (JetBrains) support is great.
  • Ionide is primarily known for its integration with Visual Studio Code, but it also extends support to other editors like Vim. Ionide for Vim offers features such as syntax highlighting, IntelliSense, project management, debugging support, and more, making it a powerful tool for F# development within the Vim editor. Additionally, there are other Vim plugins available that provide F# support, although they may not offer the same level of functionality as Ionide.
  • .NET CLI (Command Line Interface) provides command-line tools for project management, building, testing, and package management.
  • dotnet cli is extensible via dotnet-tools and dotnet tool manifest
  • F# Interactive (dotnet fsi) allows for interactive development and testing of F# code snippets within the terminal or IDE, including integration with packages/libraries (NuGet).
  • The .NET SDK offers seamless integration with other .NET languages and frameworks (e.g. C#), enabling interoperability and code sharing within the same dotnet solution, projects can be cross-referenced.

4.1 Interactive

Interactive session / REPL (read evaluate and print loop), both languages support REPL from your terminal window, to explore the language and to execute scripts.

Run a Script (interactive)

OCaml

ocaml path/to/script.ml

F#

dotnet fsi path/to/script.fsx

Script Format: .fsx (F#) vs .ml (OCaml), to load and execute a script from within other scripts (script reference) you can use in Ocaml

#use "path/to/script.ml"

In F#

#load "path/to/script.fsx"

This command will load and execute the specified OCaml/F# script file within the interactive session.

Referencing Packages (from scripts)

.NET F# (.fsx) with NuGet: In .NET F# scripts (.fsx files), you can reference NuGet packages using the #r directive followed by the package name and optionally the version number. Here's how you would do it:

#r "nuget:PackageName,Version"

For example:

#r "nuget:Newtonsoft.Json,12.0.3"

This directive tells the F# compiler to reference the specified NuGet package and include it in the script’s execution environment.

In OCaml, referencing packages within script files (.ml) is not possible as in .NET F#. While you can use OPAM to install and manage OCaml packages for projects, there’s no built-in mechanism for directly referencing packages within OCaml scripts.

We could achieve something similar by abusing the ocamlfind tool, which is commonly used to locate and link OCaml packages…

ocamlfind ocamlc -package PackageName -linkpkg -o output_script input_script.ml

This command tells ocamlfind to compile the OCaml script (input_script.ml) and link it with the specified package (PackageName). The resulting executable script will have the specified package linked and available for use.

In summary, F# scripts (.fsx) support referencing packages using the #r directive, while OCaml doesn’t provide this feature directly within its syntax. A simple reason could be that in OCaml there is no differentiation between script or source, always .ml whereas F# applies different behaviours if a file is executed as part of an interactive session as part of a .fsx file, or not. In F# .fs source files are sources to be compiled, they can also be referenced in scripts.

4.2 New (compiled) project

Ocaml/Dune/Opam

// dune is a build system also aware of opam, merlin and other ocaml features
dune init proj project_name


// or the opam way
opam switch create project_name ocaml-base-compiler

.NET (F#)

dotnet new console --lang F#

Build and Test

Dune has rules that precisely capture how the OCaml toolchain works. It is able to interoperate with most of the existing tools like OPAM, merlin, reason, and js_of_ocaml.

dune build
dune test

.NET (F#)

dotnet build
dotnet test

4.3 Package Management

Opam (Ocaml)

opam list -a         # List the available packages
opam install lwt # Install LWT
opam update # Update the package list
opam upgrade # Upgrade the installed packages to their latest version

.NET (F#)

dotnet list package
dotnet add package // installs or updates a single package
dotnet outdated // global tool: https://github.com/dotnet-outdated/dotnet-outdated
dotnet outdated -u // options can be provided to control policies

4.4 Project Configuration and Files

dune File:

(lang dune 2.8)
(name my_project)

(library
(name my_library)
(public_name my-package)
(libraries
some_dependency
another_dependency))

Opam File (opam):

opam-version: "2.0"
name: "my-package"
version: "1.0.0"
depends: [
"some_dependency"
"another_dependency"
]

.NET F# project file .fsproj

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SomePackage" Version="1.0.0" />
<ProjectReference Include="Path/To/Project.csproj" />
</ItemGroup>
</Project>

Comparison

  • Dune and Opam project files use a custom DSL (Domain-Specific Language) with a simple and concise syntax tailored for OCaml projects.
  • .fsproj files use XML syntax defined by the .NET SDK, which can be more verbose and structured compared to Dune and Opam files.
  • Dune and Opam are the primary tools for managing OCaml packages and dependencies. Dune is used for building and managing project configurations, while Opam handles package installation and dependency resolution.
  • .fsproj files use NuGet for package management, which is the default package manager for .NET projects integrated in the dotnet sdk.
  • Dune and Opam project files typically coexist within an OCaml project directory, providing a unified configuration for managing project dependencies and build settings.
  • .fsproj files are the primary project files for .NET projects and are used to define project settings, dependencies, and build configurations. They are typically located within the project directory alongside source code files.

In addition to dotnet and .fsproj F# also supports Fake for build (with a custom DSL in F#) and Paket for package management, they are community-maintained projects, so they are not the standard across the whole dotnet ecosystem but they are frequently used in F# community projects. Both Fake and Paket integrate with the existing dotnet ecosystem, extending capabilities and features with a more functional approach. For F# beginners I do not recommend using them in general unless there are specific use cases for it (personal opinion).

4.5 Example Solutions (multi project)

Dune solution (with Opam)*

*Note: this part is not super clear, feel free to file a comment down below!

MyDuneSolution/
├── dune-project
├── MyOCamlProject/
│ ├── dune
│ ├── myOCamlProject.ml
│ ├── myOCamlProject.opam
├── MySecondOCamlProject/
│ ├── dune
│ ├── mySecondOCamlProject.ml
│ ├── mySecondOCamlProject.opam

.NET solution, F# + C# for example, mixed language is possible among .NET languages, and cross-reference is possible.

MySolution/
├── MySolution.sln
├── MyFSharpProject/
│ ├── MyFSharpProject.fsproj
│ ├── Program.fs
├── MyCSharpProject/
│ ├── MyCSharpProject.csproj
│ ├── Program.cs

4.5 Tools performance

  • In general, Dune tends* to have faster compilation times compared to .NET projects built with dotnet build.
  • Dune’s efficient build system and native code compilation contribute to faster build times, especially for smaller projects or projects with simple module dependencies.
  • .NET projects, especially those with larger codebases or complex dependencies, may have longer compilation times due to additional processing steps and language features.

In summary, while both dotnet build and dune build are efficient build tools, Dune tends to have faster compilation times for similarly sized projects, particularly for OCaml projects with well-defined module dependencies. However, the actual compilation time comparison may vary depending on the specific characteristics of the projects being built.

5. Raw Performance

While OCaml bytecode might generally have faster performance than .NET, this is not always the case, as .NET GC is also highly optimized and also depending on the context Aspnetcore/Kestrel for example might overperform other frameworks for HTTP and serving web workloads in default scenarios. Rarely performance differences between these 2 languages will be a bottleneck in developing a real-world application.

In general, for performance we always need actual benchmarks and also performance may greatly vary case by case code optimization is always possible within any language, and some optimizations are language-specific. We might say that for common web backend use cases, both languages are fast enough, if you are more interested in performance, set up your own benchmark!

Below are some sample benchmarks between OCaml and F#, where F# is fastest in some cases, and Ocaml in others. in general F# and .NET seem to use more memory than Ocaml, but recent improvements have been made in .NET with AOT that can greatly reduce the resource footprint of .NET workloads.

Interestingly, also F# can compile to Rust via Fable (alpha) so that might lead to performance gains if one wanted to exit the .NET arena.

6. Unique features of F#

*Note: In OCaml, the pipeline operator (|>) used to be not part of the standard library, however, it is now shipped in the standard library, ref,
so this makes the 2 languages even closer to each other now, cc Yawar λmin thanks for the note!

7. Unique features of OCaml

  • generic modules and functor-based module system
  • faster compilation toolchain
  • native binary by default (with GC, a bit like Go)
  • potentially could run on JVM too, but didn’t follow up on this
  • C interop is facilitated through the ocaml C API, which allows developers to call C functions from OCaml code and vice versa. This interop capability enables OCaml developers to leverage existing C libraries, take advantage of performance optimizations, and access low-level system functionality.
  • Melange: compiles Ocaml to JS a bit like Fable for Javascript (only JS supported)
  • Reason: technically reason is not Ocaml but Reason is a supported language for developing Meta applications (Ract), so if you are mostly working with React and meta apps/ecosystem, this might be a great choice for you (although also F# supports React via Fable-react and Feliz).
  • More? File it in the comments section, Thank you!

References, resources, food for thought

--

--