A Gentle Introduction to Functions in ReasonML

Yawar Amin and Kamon Ayeva · Mar 13, 2019 00:00 · 2007 words · 10 minute read

Reason is a new syntax and toolchain powered by OCaml and developed by Facebook. It’s a familiar syntax to many developers as it is closely aligned with Javascript.

In this post, we will cover an introduction to functions in Reason and how to use them effectively. Functions wrap calculations and actions for reusability, so it makes sense that they’re among the most heavily used features in any programming language. So, by taking advantage of Reason’s type system and functional programming techniques, you can design functions for maximum effectiveness.

But first, what is a function? A function in type theory and mathematics has a formal definition, but you can think of it as a formula for calculating an output given an input. In Reason and other statically-typed functional programming languages, functions always have an output, even if they don’t actually calculate anything. You’ll examine how to express these inputs and outputs, but you first need a basic understanding of function types and properties.

Function types and other useful properties

In Reason, functions have very specific types and, just like other values, functions of different types can’t be substituted for one another. The basic type of every Reason function is as follows:

a => b

Read this as “a arrow b”.

As you can see, the input, a, and the output, b, can be any type (even the same one). This basic function type, with a single input and a single output, gives rise to every other function type in Reason. You’ll see how this happens shortly, but first here are a couple of useful functional programming concepts that are important in the type-driven world. Referential transparency The first property, called referential transparency (or RT), means that a function will always produce the same output, b, for a given input, a, no matter how, when, or how many times you call it. This means that a function can’t behave unpredictably; you must be able to predict its output for every input, purely like a mathematical formula.

For example, the following is a non-referentially transparent function:

/* [xDaysAgo(x)] returns the time [x] days before now, in ms since Unix
    epoch. */
let xDaysAgo(x) =
  Js.Date.now() -. float_of_int(x) *. 24. *. 60. *. 60. *. 1_000.;

You can’t tell what the output will be for any given input, x, because that depends on the date and time the function is called. The problem instead is the hidden dependency on the current date or time. One solution is to remove the dependency by passing it in as a function argument, as follows:

/* [xDaysAgo(now, x)] returns the time [x] days before [now], in ms
    since Unix epoch. */
let xDaysAgo(now, x) =
  now -. float_of_int(x) *. 24. *. 60. *. 60. *. 1_000.;

The immediate benefit is that the function is easier to test, but the bigger benefit is that functions such as this in the codebase make it easier to reason about. Reasoning about code (also known as equational reasoning) means being able to substitute actual values in place of function arguments, and just like a math equation, evaluate to the result by simplifying it. This sounds like a trivial benefit, but when used over a codebase, it can be a powerful technique for ensuring transparency.

Realistically speaking, you can’t make the entire codebase referentially transparent (unless you use advanced techniques such as effect types). You can, however, push out the non-RT operations to the edges of the program. For example, you can call the (second) xDaysAgo function with either the result of a call to Js.Date.now() or a date value passed in from somewhere else. This is a simple but effective form of dependency injection (passing in values to a program instead of letting the program try to get the values itself).

Function purity

The second important property that you must try to achieve is purity. This concept means that to the caller (that is, the code that calls it) and the outside world, a function has no impact other than evaluating to its result. You say that the function does not have any observable effects. Observability is the crucial thing here; there may well be effects happening and contained inside the function (such as mutation), but the caller doesn’t and cannot know about them. The following is an example of a pure function that mutates internally but not observably:

let sum(numbers) = {
  let result = ref(0);
  for (i in 0 to Array.length(numbers) - 1) {
    result := result^ + numbers[i];
  };
  result^
};

If you were to add a Js.log(result^) inside the body of the for loop, the function would become impure because its effects would become observable. People sometimes disagree with what exactly observable means, especially in the context of logging the operations of otherwise-pure functions, but it’s safe to err on the side of caution and accept that any observable effect is an impurity in the function (and that’s OK, because sometimes you actually need those observable effects).

Totality

The last important property that you want functions to have is totality. This means that functions should handle every possible value of the type that they accept, which is actually trickier than it seems! For example, look at the xDaysAgo function again. What happens if x is negative or very large or small? Did you account for integer overflow? Especially when working with numbers, you need to understand their properties on the platform you’re running on top of. In this case, you’re running on a JavaScript platform such as Node.js, so all numbers are internally represented as IEEE floats (that’s how JavaScript works) and you can get pretty far before you need to worry about overflow. But consider the following trivial function:

let sendMoney(from: string, to_: string, amount: float) = Js.log(
  {j|Send \$$amount from $from to $to_|j});

Here, you’re just printing out what you want to happen. In a real application, you might want to do a money transfer. Suppose you exposed this function with an HTTP service call. What would happen if someone called the service with a negative float? The best-case scenario is that the error would be caught somewhere else; the worst is that people could make calls to siphon money out of other people’s accounts. One approach to solving this is to validate your arguments at the very beginning of the function, as follows:

let sendMoney(from, to_, amount) = {
  assert(from != "");
  assert(to_ != "");
  assert(amount > 0.);
  Js.log({j|Send \$$amount from $from to $to_|j});
};

For good measure, in this snippet, you’re doing some basic validation on the sender and receiver strings. You’re also able to get rid of the type annotations because the assertions will cause them to be inferred correctly.

From the function’s point of view, internally, it’s now a total function because it explicitly errors on the cases it doesn’t want to handle but does handle the remaining happy path. To the outside world, however, the function is still taking in raw strings and floats and failing to handle most of them. A better solution is to use more constrained types to describe exactly what the function can accept, as follows:

/* src/Ch07/Ch07_DomainTypes.re */
module NonEmptyString: { /* (1) */
 type t = pri string; /* (2) */
 let makeExn: string => t;
} = {
 type t = string;
 let makeExn(string) = { assert(string != ""); string };
};

module PositiveFloat: { /* (3) */
 type t = pri float;
 let makeExn: float => t;
} = {
 type t = float;
 let makeExn(float) = { assert(float > 0.); float };
 let toFloat(t) = t;
};
 
let sendMoney( /* (4) */
 from: NonEmptyString.t,
 to_: NonEmptyString.t,
 amount: PositiveFloat.t) = {
 
 let from = (from :> string); /* (5) */
 let to_ = (to_ :> string);
 let amount = (amount :> float);
 Js.log({j|Send \$$amount from $from to $to_|j});
};
 
sendMoney( /* (6) */
 NonEmptyString.makeExn("Alice"),
 NonEmptyString.makeExn("Bob"),
 PositiveFloat.makeExn(32.));

This snippet looks more verbose, but in the long run, it is the better solution because you can write tests for the wrapper types and their modules in isolation, get peace of mind that the types really do enforce your rules, and reuse the types instead of adding checks throughout the codebase. Here’s how it works:

  1. You set up a type whose values can only be non-empty strings. If a caller tries to construct an empty string of the type, it will fail with an exception.
  2. The type declaration says that this is a private type, meaning that you expose its internal representation, but you don’t allow users to construct values of the type. This is a useful technique when you want to semi-transparently take an existing type and restrict it in some way.
  3. Similarly, you set up a type whose values can be only positive floats.
  4. In the sendMoney function, you reap the benefit of these types by only accepting these constrained types instead of their raw variants. The function is now total because it only accepts exactly the values it works with by (type) definition.

  5. You still need to unwrap the constrained values to get at the raw ones, because ultimately you want to print the raw values. Because the types are declared as private though, you can coerce them back to their more general versions. Coercion means forcing a value of a constrained type (such as NonEmptyString.t) back to being a more general type (such as string). Coercion is completely static; if you can’t coerce something, you’ll get a compile error. Note that the syntax for coercion needs to be pretty exact, and it needs to include the parentheses.

  6. You also need to wrap the values before you pass them into the function. This is the point that can potentially fail, so you’ve moved it outside of your function implementation.

Here, you’ve used the convention of adding Exn to the names of the functions that may throw exceptions. Some people prefer to return optional values instead of throwing exceptions. This convention is idiomatic and type-safe, but is ultimately just another method of reporting errors. The key point to take away is that any possible failures have been moved out of your total sendMoney function, and other functions that use constrained types.

What a function type means

In the context of type-driven development, why are functional programming concepts such as referential transparency, purity, and totality important? The reason is that a function’s type has a well-understood mathematical meaning and breaking such rules muddy this meaning.

A function type such as a => b means that a function of this type will accept an input of type a and evaluate to a result of type b, and will do nothing else (for example, print out a log, start the coffee maker, or launch missiles). You like having this guarantee in much the same way as you like knowing that an int is just an int and not a missile launch followed by an int.

The fact that Reason allows side effects is a great pragmatic decision, but you can still strive to push the side effects to the very edges of your programs and keep their cores purely functional. Purity in the functional sense is necessary for the type of a function to be accurate. If types in your program are accurate, you can perform type-driven development with more confidence.

We hope you found this article useful, and if you are still interested, we recommend Learn Type-Driven Development - a fast paced guide for JavaScript developers for writing safe, fast, and reusable code by leveraging ReasonML’s strong static type system. Learn Type-Driven Development will help you to learn how to iterate through a type-driven process of solving coding problems using static types, together with dynamic behavior, to obtain more safety and speed.

About the Authors

Yawar Amin, is a Software Engineer by profession, with a background in Statistics and Econometrics, and Kamon Ayeva, a Web Developer / DevOps engineer who spends most of his time in building projects, using Python’s powerful scripting capabilities, add-on libraries and web frameworks such as Django or Flask.