This guide aims to introduce you to enough F#/ReScript to contribute to Dark, assuming you already know Dark. F# and ReScript are very similar languages, both derived from OCaml. We currently use ReScript using the OCaml format (*.ml files) for the client. We use F# for the backend, which is very similar to Dark as well.
Some simple F#/ReScript code
Dark and F# and ReScript are very similar. Here's an example function in F#:
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:
In ReScript this would be written (note the
in at the end of each line):
As you can see, apart from the function signature, the only difference is that
let statements in ReScript have an
in at the end of the line.
Dark vs ReScript vs F#
Dark, ReScript, and F# are all influenced by OCaml. Though Dark is currently a subset of these languages, Dark will continue to grow some more of their features. We'll discuss the similarities and differences as we go through language features.
ReScript/F# are strongly typed-languages. Dark aspires to be, but it doesn't have a type-checker yet. This shows the biggest difference in between working in these languages, that the compiler will refuse to compile if the types are wrong.
ReScript/F# have 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
We'll discuss declaring types below.
Functions in ReScript and F# are defined in the outer scope. Type signatures are optional but we always use them:
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)
Note: for implementing the standard libraries, we do not use Tablecloth as it is
still pretty new and may be in flux. Instead, we try to make sure that we use
.NET, FSharp.Core, or if necessary, the FSharpPlus library.
int is the same in Dark and OCaml, same syntax, same meaning. Note that
ints are 31 bits in ReScript, 32 bits in F#, and infinite precision in Dark.
float is the same in Dark, ReScript, and F#, both of them are 64-bit
floating point numbers.
Like in Dark, there are special operators to work on floats in ReScript.
To convert from floats to ints use
In F#, standard operators work on floats too.
Like in Dark,
bools in F# and ReScript are either
Strings are Unicode in Dark, ReScript and F#. While you're unlikely to hit differences in practice, they do actually use a different in-memory representation, with Dark using UTF-8, rescript using the browser's built-in UCS-2, and F# using .NET's UTF-16 (which is very similar but not quite the same as UCS-2).
Both ReScript and F# support string interpolation. In F#:
Dark does not currently support String interpolation.
Lists in Dark, F# and ReScript are almost the same. In ReScript and F#, lists
; as separators, like so:
(However, F# allows separators to be omitted which the list elements are lined up vertically, as it uses indentation as the separator).
While Dark technically allows you to create lists that have different types in them, ReScript and F# emphatically do not.
To type check a list in ReScript, you specify its type like so:
which is a list of ints. In F#, you use
int list is still
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 ReScript has unusual syntax:
F# uses the same syntax, though it allows you to use indentation instead of separators:
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 ReScript/F#, and are updated using an unusual syntax:
Note that records in Dark are really dictionaries, which is why you update them
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
ReScript/F# (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 ReScript/F#, although we don't currently use
that very often.
if statements in F#/OCaml are extremely similar to Dark, including that they
bools as the condition, and in their syntax.
F#/OCaml, in keeping with its odd syntax, have some unusual operators. Most
importantly, the equality operator is
= (that's just one equals sign), whereas
in most languages it's
== (including Dark).
= is very strict equality,
=== in languages that have that, such as JS.
ReScript/F# also have a
== operator, but you should never use it.
<> for inequality (
Dark). In ReScript, most of the comparison operators (such as
etc) only operate on integers, and not on floats. In F#, they work on both ints
Dark has a
match statement that is very similar to F#/ReScript, with slightly
with keyword, and starting the patterns with
F#/ReScript also support more powerful
matches, for example multiple patterns
can match a single branch:
ReeScript/F# also support 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
ReScript/F# support the
Result types and we use them a lot.
However, the constructors for Option are named differently; both languages use:
None instead of
These enums are typically called "variants". We use them frequently, especially
to represent expressions. For example in
t (it's a common convention to name the main type of a module
be one of
EInteger takes two parameters,
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:
ReScript and F# support lambdas and we use them frequently. They have a different syntax to Dark, F# uses:
It's very common to use functions like
List.map which have a parameter called
f which take a lambda. In F#, above, that parameter is passed like any other.
In ReScript, those parameters are passed as labelled variables:
OCaml has pipes which are the same as in Dark, except that in ReScript and F#, the pipe goes into the final position (in Dark it goes into the first position):
Dictionaries (hash-maps, etc) are typically called
Map in F#/ReScript. In
ReScript, they are slightly challenging to use, which is one reason you won't
see them used as much as they really should be.
F#/ReScript have 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
F# and ReScript also have 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 when an exception could be thrown.
F# and ReScript support imperative programming which Dark does not support yet. There are mutable values called refs, that can be updated:
To go along with it, F# and ReScript have
while loop, allowing you
to use imperative programming in places where it's clearer to do so:
ReScript 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:
These are not in F# or Dark.
ReScript also supports optional parameters, which F# and OCaml do not.
By default, F#/ReScript 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 (F#/ReScript 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:
ReScript 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 client).
F# does have modules, but it does not support any of the complex stuff that ReScript does.
Classes and Objects
ReScript 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 ReScript JS interop code compiles it to direct OO in JS).
F# has OO that is used to interact with .NET types, and call C# libraries. We do not use it very much but we do use it some. Defining a method:
ReScript vs Bucklescript/ReasonML?
ReScript is the new name (as of 2020) for what was sometimes called Bucklescript and sometimes called ReasonML. You may see references to Bucklescript in our codebase (including the prefix "bs").
Because Dark's backend was in OCaml, we wrote the frontend using the alternate OCaml syntax for ReScript, which is not well documented. We will probably switch at some point to the ReScript syntax.