X Marks the Spot (Part 3): A Strong Fit

X Marks the Spot (Part 3): A Strong Fit

Chris Wilson
Chris Wilson

April 30, 2019

This is part three (of three) in a series about ReasonML and typed functional programming. You can read part one and part two here.

A Strong Fit

Last time we looked at an app written using ReasonReact. This is a take on React, which nicely integrates into Reason the language. This article will take a different turn. If you just came for the code, then this is a great time to stop reading. Seriously, I'm getting out the soapbox! I am going to talk about things that come up while writing "business applications" that fit nicely into Reason's features. Perhaps a good summary would be:

Statically-typed functional languages deserve a crack at implementing business apps.

You may find that you really like it.

Sketching Out the Domain

Now I'm going to lay out why I think Reason/OCaml, or languages staking out similar semantic real estate, rule at business apps. Scott Wlaschin lays out the case for writing BLOBAs in functional languages. Everything that he says about F# is true for Reason and OCaml as well. These languages are excellent at sketching out and then crystallizing domain entities.

As an example, a domain expert could probably figure out this piece of code1:

type suit =
	| Spades
	| Hearts
	| Diamonds
	| Clubs;

type rank =
	| Two
	| Three
	| Four
	| Five
	| Six
	| Seven
	| Eight
	| Nine
	| Ten
	| Jack
	| Queen
	| King
	| Ace;

type card = (rank, suit);
type hand = list(card);
type deck = list(card);
type player = {
	name: string,
	hand,
};
type game = {
	deck,
	players: list(player),
};

/* Functions */
type deal = deck => (deck, card);
type pickupCard = (hand, card) => hand;

The domain expert would probably be able to see enough of the way that the entities "fit together" that they could do some validation—all before we've written any real code.

Being able to find out fundamental gaps or flaws in the domain early on is helpful.

Guarding the Perimeter

In my experience with business applications, a few key concerns2 are ever-present:

  1. Ingesting data from various external sources via JSON or CSV.
  2. Persisting and loading data from storage like relational databases, flat files, or key-value stores. This can overlap with (1) as well.
  3. Presenting data in marked-up form, like HTML.

These seemingly disparate activities all share a common theme. In each case we must interact with external data. Whether the data arrives over the network, via forms, or uploads, it all arrives as an untyped blob. Our task then is to extract meaningful values from a soup of undifferentiated bytes. Static types and parsers provide a clear demarcation between outside the perimeter and the interior of the application.

This isn't a new idea. The concept of Ports and Adapters is well-established and describes a similar notion of creating a bright line between what's core to the application and what's a means to access that external data safely.

An X Marks the Spot language brings powerful tools to bear on this problem. It opens up the possibility of enforcing inside/outside separation at the language level. It isn't so much a new strategic approach as it is just a really effective tactic.

Bs-json is a library that takes the aforementioned approach. To use this library, we first create a "decoder," which is a function from JSON to some business object, e.g. a "product." The library then has a function, parse, which converts from the raw JSON string to a typed JSON value. I've used a small wrapper function, runParse, to put the two steps together: string → json, and json → option(product). And voilà, we have a function that follows the guarding the perimeter philosophy. We convert directly from a string, which may or may not contain valid JSON, to our product value. Since product is wrapped in an option, the type system also forces us to handle the possibility that we might have got junk instead of valid JSON.

type product = {
		sku: string,
		upc: string,
		name: string,
		price: int,
		discount: option(float),
};

module Decode = {
		let product = json => {
				open! Json.Decode;
				{
						sku: json |> field("sku", string),
						upc: json |> field("upc", string),
						name: json |> field("name", string),
						price: json |> field("price", int),
						discount: json |> optional(field("discount", float)),
				};
		};
};

let jsonBlob = {|
		{
				"sku": "KD9KJV",
				"upc": "065100004327",
				"name": "Widget",
				"price": 5000
		}
|};

let runParse = (decoder, json) =>
		Json.parse(json) |. Belt.Option.map(decoder);

switch (runParse(Decode.product, jsonBlob)) {
| None => Js.log("No product was found")
| Some(product) => Js.log(product.sku)
};

It's okay if the above doesn't fully make sense. The gist is that we specify what we expect to see in our app and then back that up with a type. We then have functions, which can either give us that value or fail. There's no half-valid hashmap floating around the sensitive interior of our app.

Concretizing Failure

An X Marks the Spot language also changes the way we think about working with failure. Rather than an external concern of the runtime, we can internalize failure conditions and make them part of the API of our app. This has the effect of reining in the worst sort of error. We can finally get a handle on undefined is not a function.

The approach is that instead of returning a value or raising an exception, we instead return a value that represents what happened. We've seen this a little bit with the option type.

type option('a) =
		| Some('a)
		| None;

An option is either Some value, or None (no value). It is different from a value that's not an option:

let x: option(int) = Some(1);
let y: int = 1;

It is a compile-time error to add them together:

x + y /* this doesn't compile:

[bucklescript]
This has type:
 option(int)
But somewhere wanted:
 int
*/

This has the subtle, but immense, benefit of stopping us from sweeping the possibility of failure under the rug. It also lifts this concern into the type of a function. At a glance we can see if a function returns option(...) and act accordingly. An un-checked exception is a little bit of a fib, at least as far as the type system is concerned. Such a function is promising that it can always deliver, while in reality it cannot.

We use little failable functions all the time. Every time we do a lookup in a hashmap or dictionary, we might come up empty:

irb(main):006:0> some_hash[:foo]
=> nil

How many times has this happened! It's much better to acknowledge this possible missing value and say so in the type of the function.

Modules

There's so much to say about modules that it could easily fill another blog post. Modules are a namespace with an interface, somewhat like a class, but without the bundling of state and behavior. A module is a collection of types, values, and functions that all cohere in some domain-meaningful way. Modules can also be higher-order—there is a structure known as a functor, which is essentially a function from modules to modules. This allows us to customize the behavior of a module, or make it abstract over some interface. Whew! That's a lot, let's look at an example:

module Account: {
	type t;
	let empty: t;
	let deposit: int => t => t;
	let withdraw: int => t => option((int, t))
} = {
	type t = int;
	let empty = 0;
	let deposit = (amount, account) => amount + account;
	let withdraw = (amount, account) => {
			switch() {
					| _ when amount > account => None
					| _ => Some((amount, account - amount))
			}
	};

Because there is a type signature attached to this module (lines 2-5), the type t of Account is abstract. Though internally Account is using an int to represent account balances, from outside this module the type is opaque. This opaqueness can be used to enforce the interface. With Account, the only way to get a hold of a new account type is through the Account.empty value, which has the type Account.t.

Modules are closely related to objects in object-oriented languages but, because modules do not use subtyping (subclassing), their behavior and use is somewhat different. One tends to depend on the interface of a module as a whole unit rather than depending on a concrete object collaborator (or the object's descendants), as you do in OO programming. Object-oriented design has also adopted a similar compositional approach: "favor composition over inheritance." This is a different point in the design space, but one that it seems fits very well with business software.

Summary

This is a quick tour of a deep language. OCaml, and Reason by extension, has been around for over 20 years. OCaml is stable, well-tested, and has been applied successfully to a myriad of different tasks. OCaml is used in many industries from finance to education. The language has been used to do everything from writing a unikernel operating system, to a JavaScript typechecker. Reason, by adding a new veneer—one that does not at all detract from the underlying language—promises to pump new energy into the language and ecosystem. Reason helps to make all that built-up know-how, static safety, and functional programming productivity more accessible to an audience familiar with JavaScript.

Those who are used to JavaScript syntax will find Reason to be a little bit different, but still familiar. And if tweaking syntax to be more familiar is all it takes to get more mainstream adoption, why not? The combined energy and and attention of the JS community is like a hothouse for the growth of a language ecosystem.

Typed FP has an awful lot of good parts that deserve more widespread adoption. I'm hoping that Reason and ReasonReact will help to build bridges that will make that journey possible.


  1. This example is drawn from https://fsharpforfunandprofit.com where it was presented in F#

  2. And rich sources of bugs