This guide aims to introduce you to enough OCaml to contribute to Dark, assuming you already know Dark.
Some simple OCaml code
Dark and OCaml are very similar. Here's an OCaml function:
This is a function called
someFunction, which takes one argument, an
intArgument, and returns a
string. Three variables are
defined in the body, first a string, then an int, then a float, and
finally we call the
anotherFunction function with all three
parameters as arguments.
In Dark this would be written:
As you can see, apart from how the function is written, the only
difference is that
let statements in OCaml have an
in at the end of
Dark vs OCaml
OCaml is a very large influence on Dark, and Dark will continue to grow some more of OCaml's features. We'll discuss the similarities and differences as we go through language features.
Since Dark doesn't let you type syntax, it doesn't have syntax errors.
OCaml has syntax errors, and the error messages are not good. I tend to
make sure that my code syntax checks by running
oCamlformat in my
editor on save: if it reformats, then the syntax was good.
OCaml is a strongly typed-language. Dark aspires to be, but it doesn't have a type-checker yet. This shows the biggest difference in working in OCaml, that the compiler will refuse to compile if the types are wrong.
OCaml has type-inference, which means that the compiler will try and figure out what the types are. This is frequently the source of bad compiler messages, often it will tell you something which seems wrong because it guessed wrong about certain types.
Usually type errors actually contain useful information, but they need to be read very carefully to find it.
We've found the best way to debug incorrect types is to add type annotations to everything. We add them to all functions (we didn't always do this, but we do now, but we are now), including all parameters and return types (see example below).
You can actually add types in many places where they aren't required, such as variable definitions:
x here, despite being a normal variable definition, has a type
signature. OCaml allows this in many places, and it's useful for
tracking down these errors.
We'll discuss declaring types below.
Functions in OCaml are defined in the outer scope. Type signatures are optional in OCaml but required in the Dark codebase:
myFunction has two arguments,
arg2, which are an
string respectively. It returns a
Like in Dark, the body of a function is just an expression, and it automatically returns the result of that expression.
(see below for more details on functions in OCaml)
A lot of the backend uses Core, one of the most popular standard libraries for OCaml. The Jane Street Core library has three flavors: Base, Core_kernel and Core, each with progressively more expansive functionality. The native version of Tablecloth is built on top of "Base". The Dark backend typically uses Core_kernel as we have not transitioned to Tablecloth fully.
Note: we try to use Core_kernel directly when implementing the language and standard libraries, as Tablecloth is still in flux and has not yet reached stability.
int is the same in Dark and OCaml, same syntax, same meaning. While Dark
intends to one day support infinite precision integers, today it uses 63-bit
integers, which is the same as OCaml.
float is the same in Dark and OCaml, both of them are 64-bit floating point
In OCaml, there are special operators to work on floats:
To convert from floats to ints use
Like in Dark,
bools in OCaml are either
A String in Dark is Unicode (UTF-8), while a
string in OCaml is just bytes
(we use the
Unicode_string module to convert them to Unicode).
Lists in Dark and OCaml are almost the same. In OCaml, lists use
; as separators, like so:
While Dark technically allows you to create lists that have different types in them, OCaml emphatically does not.
To type check a list, you specify it's type like so:
int list, which is a list of ints.
Records are mostly used as objects are in most languages. Like Dark, they only have fields, not methods, and you use functions to manipulate them.
A record in OCaml has unusual syntax:
Note that they use
= to connect a field and a value, and
; as row
separator. The types of the fields do not have to be declared.
Records are immutable, like almost everything in OCaml, and are updated using an unusual syntax:
Note that records in Dark are really dictionaries, which is why you
update them with
Dict::set. We're trying to figure out how to split
records and dictionaries apart better in Dark, after which they will be
more like OCaml (though hopefully with better syntax).
Type definitions for records look like this:
lets have a slightly different syntax to Dark:
in at the end is required.
let also allow destructing in OCaml, although we don't currently use that
if statements in OCaml are extremely similar to Dark, including that they
bools as the condition, and in their syntax.
OCaml, in keeping with its odd syntax, has some unusual operators. Most
importantly, the equality operator is
= (that's just one equals), whereas in
most languages it's
= is very strict equality, equivalent to
=== in languages that have that, such as JS.
== is the same as OCaml's
=. OCaml also has a
== operator, but you
should never use it.
OCaml's inequality operator (
!= in Dark) is
<>. Most of its comparison
operators (such as
<=, etc) only operate on integers.
Dark has a
match statement that is very similar to OCaml's, with slightly
with keyword, and starting the patterns with
OCaml also supports more powerful
matches, for example multiple patterns can match a single branch:
OCaml also supports the
Be careful of very subtle bugs when combining multiple patterns with
clauses: the entire pattern will fail if the pattern matches when the clause
Dark has a handful of enums for
Error. In the future we will expand this to allow user-defined types as well.
OCaml supports the
Result types and we use them a lot. However, the constructors for Option OCaml are named differently:
OCaml calls enums "variants". We use them frequently, especially to represent expressions. For example in
t (it's a common convention in OCaml to name the main type of a module
t) must be one of
EInteger takes two
id and a
string (we use a string to represent integers as
Bucklescript doesn't have a big enough integer type).
To create a
t, you'd do something like this:
To get values from them, you pattern match:
OCaml supports lambdas and we use them frequently. They have a different syntax to Dark:
It's very common to use functions like
List.map which have a parameter called
f which take a lambda.
OCaml has pipes which are the same as in Dark, except that in OCaml the pipe goes into the final position (in Dark it goes into the first position):
Dictionaries (hash-maps, etc) are typically called
Map in OCaml, and are
unfortunately pretty hard to use, which is one reason you won't see them used
as much as they really should be.
OCaml has a
unit type, whose only member is
(). That's an actual value, for example, all this is valid code:
It's typically used to pass to a function which is impure but doesn't have any
meaningful arguments, such as
gid () (which generates IDs).
Typically we use
Options for error handling. You'll very commonly see something like
To find out if a function goes on the error rail, we search for a function,
which returns an Option. We then use a
map to operate on the option, and
finally choose a default in case the Option returned
OCaml also has exceptions - we hardly use them in the client, but unfortunately use them a little bit on the backend, which we'd like to do less of.
Unfortunately, it's hard to tell in OCaml when an exception could be thrown.
OCaml code goes in
.ml files - each file is a module. OCaml also has interface
.mli) which describe the module in the
.ml file of the same name.
While they aren't necessary, they make it easier to know what functions are
unused, they make APIs clearer, and they make compilation faster. As such, Dark
is moving towards an
.mli for each
OCaml supports imperative programming which is not in Dark yet. There are mutable values called refs, that can be updated:
To go along with it, OCaml has
while loop, allowing you to use
imperative programming in places where it's clearer to do so:
Functions support named parameters, which you might see called like this (note the
These are useful as a named parameter can be placed in any order (this is also useful for piping).
You declare functions with named parameters like so:
OCaml also supports optional parameters
By default, OCaml functions are not recursive: they cannot call themselves. To allow a function to call itself, add the
Similarly, if two functions need to call each other, they need to be aware of each other (OCaml programs require all functions to be defined before they are used). The
and keyword allows this:
Partial application / currying
Occasionally you'll see a function called with fewer arguments than it has parameters:
This is called "partial application", in that the function is only partially called (this is often called Currying in the functional language community). This just means that some parameters are filled in, and you now have a function which can be called with the remaining parameters:
This is the same as if it were defined as:
OCaml has a complex module system, which takes some time to grasp. Modules can have parameters, have inheritance, and each other these features uses a complicated, difficult to grasp syntax.
We only barely use modules in the Dark codebase, so here's what you need to know:
all files are automatically modules. Note that in the backend, modules need to have their directory names included, but not in the client.
using a module is simple:
- the syntax of creating a module is also straightforward:
Types modules at the top of all
files (which in turn open other modules, like
Tablecloth on the
Classes and Objects
OCaml supports traditional object oriented programming, though it's not used very much and very discouraged. The only place we really use it for interacting with JS (the Bucklescript JS interop code compiles it to direct OO in JS).
OCaml vs ReasonML vs Bucklescript - what's the difference?
The backend is in OCaml and the frontend is in Bucklescript. Also, something about ReasonML. What's the difference? The simplest answer is that these are all the same.
Bucklescript and OCaml are both compilers:
- the native OCaml compiler compiles programs to binaries. The backend uses native OCaml.
- the Bucklescript compiler compiles programs to JS. The editor uses Bucklescript.
ReasonML and OCaml are both syntaxes:
- ReasonML is a JS-like syntax for the OCaml language
- OCaml has a default syntax (we use this in the Dark repo for both the backend and the client)
ReasonML is also often used to refer to the community around compiling to JS using Bucklescript, and associated technologies.
Again, the simplest mental model is that all the words mean the same thing.
- the Dark backend uses the native OCaml compiler and the OCaml syntax
- the Dark client uses the Bucklescript compiler, and the OCaml syntax.