Type Annotation in ReasonML
J David Eisenberg
Posted on December 22, 2019
One of the most powerful features of ReasonML is its type inference system. In code like this:
let age = 42;
let price = 10.66;
let word = "reason";
let isValid = true;
let hours = [10, 2, 4];
let focalLength = (objDist, imgDist) => {
(objDist *. imgDist) /. (objDist +. imgDist);
};
ReasonML figures out the type of each of these bindings. If you’re using an editor like Visual Studio Code with the reason-vscode extension, you can see what ReasonML has inferred:
The type inference system does such a good job of figuring out what types you’re using that you can write an entire program without having to specify any types. But type inference is still there, looking over your shoulder and letting you know if you do something wrong, as in line two:
We've found a bug for you!
/home/david/annotation/src/Demo.re 2:19-21
1 │ let age = 42;
2 │ let total = age + "3";
3 │ let price = 10.66;
4 │ let word = "reason";
This has type:
string
But somewhere wanted:
int
Sometimes, though, there are situations where the type inference system can’t figure out what you need. Sometimes there’s an ambiguous situation (two different record types with the same fields or similar ones in different modules) where type inference makes a choice—but not the one you want. And sometimes you just plain want to do your own type annotation. Here’s how to do it.
Annotating Value Bindings
For bindings to a value, you follow the variable name with a colon and the value’s type. Here are our original value bindings with explicit annotation:
let age: int = 42;
let price: float = 10.66;
let word: string = "reason";
let isValid: bool = true;
let hours: list(int) = [10, 2, 4];
While you can annotate value bindings, almost nobody does this. In most cases, the expression on the right-hand side makes the type sufficiently clear that adding the annotation won’t give you an exponential gain in clarity.
However, many people do annotate function bindings.
Annotating Function Bindings (Method 1)
Function binding annotation follows the same pattern, with the type information between the function name and the equal sign:
- Start with a colon
- In parentheses, specify the parameter types
- Add
=>
- Specify the return type
Here’s the annotation for the focal length function:
let focalLength: (float, float) => float =
(objDist, imgDist) => {
(objDist *. imgDist) /. (objDist +. imgDist);
};
Annotating Function Bindings (Method 2)
The preceding method with the type information separate from the parameter list and function body is familiar to people coming from a language like Haskell or Elm.
If you’re coming from a language like Java or TypeScript or Flow, you’re used to seeing type information attached each individual parameter. ReasonML supports that kind of notation as well:
let focalLength = (objDist: float, imgDist: float): float => {
(objDist *. imgDist) /. (objDist +. imgDist);
};
Annotating Functions with Labeled Parameters
Consider this un-annotated function to calculate the total price, given a quantity, unit price, and tax as a percent. This function uses labeled parameters, specified with the ~
. When you call the function, you need to give the parameter name, but you can give the parameters in any order you like:
let totalPrice = (~qty, ~unitPrice, ~tax) => {
(float_of_int(qty) *. unitPrice) *. (1.0 +. (tax /. 100.0));
};
let price1 = totalPrice(~qty=5, ~unitPrice=34.95, ~tax=7.5);
let price2 = totalPrice(~unitPrice=15.00, ~tax=5.0, ~qty=12);
When you annotate this function with the type information separated from the function definition (method 1), you need to name the parameters in the same order as in their declaration in the function:
let totalPrice: (~qty: int, ~unitPrice: float, ~tax: float) => float =
(~qty, ~unitPrice, ~tax) => {
(float_of_int(qty) *. unitPrice) *. (1.0 +. (tax /. 100.0));
};
When using parameters-with-their-types (method 2), add the type information exactly as you did with unlabeled parameters.
let totalPrice = (~qty: int, ~unitPrice: float, ~tax:float): float => {
(float_of_int(qty) *. unitPrice) *. (1.0 +. (tax /. 100.0));
};
Which Method to Use?
The main advantage of the separate specification (method 1) is that this is the format you use when creating a .rei
(ReasonML interface) file. You use .rei
files to specify an API for modules that you would like other people to use.
The main advantages of the parameter-with-type specification (method 2) are familiarity and the fact that you don’t have to specify a type for every parameter. If a parameter’s type is too complicated for you to figure out, you can leave it out and let the type inference system take over for you.
From what I’ve seen in code written by others, most people let the inference system do all the work. When they do annotate, they use method 2.
Should You Annotate?
If you’re coming from a programming language where you have to specify types, I recommend that you annotate your ReasonML code as well.
If you’re coming from an untyped language, I recommend that you annotate your code as you’re learning ReasonML. This will get you used to thinking through exactly what kinds of input and output your functions need. Don’t worry about making mistakes—the type inference system will keep you honest!
Please let me know your thoughts in the discussion.
Posted on December 22, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.