Power Rows, part 2

Last week, I explained how the Power Rows bring together the easiness of records (named objects in JavaScript) and the cleanliness of strongly statically typed records. And we told there was more to it. Some expert readers indeed knew that the features we mentioned in part 1 are something along the lines of old research papers. What is unique about Power Rows, hence the name, is their ability to also handle variants.

Variants?

Also called tagged unions, variants are a way to express that a given value is either of this type, or another. For instance, we can express that a value of type material is either

wood or metal or plastic

In OCaml, the above type is defined as:

type material = Wood | Metal | Platic

and used like this:

let value = Wood

let burns = function
| Wood | Plastic -> true
| Metal -> false 

Combined with type variables, they become a very powerful way to construct datastructure. For instance, the type option, defined as

type 'a option = None | Some of 'a

allows to build options of something (something being named ‘a here)

let winner = Some "foo"
...
let congrats = function
| Some winner -> printf "Congratulations %s" winner
| None -> printf "No winner. Try again!"

This is much nicer and proper code than something like:

var winner = "";
...
function congrats(winner) {
    if (winner == "") { return "No winner. Try again!" }
    else return "Congratulations " + winner
}

because we might have an empty name given by the user or any other reason which would result in displaying the “No winner” message in error.

Variants in Opa

Opa supports variants straightforwardly, without even dealing with types

function burns(m) {
    match (m) {
        case {wood}
        case {plastic}: true;
        case {metal}: false;
    }
}

Variants in Opa are polymorphic, meaning that you can reuse labels such as { wood } in other types. The beauty of variants is that if you somewhere write:

burns({ leather })

the compiler will complain before you can run the program, at compile time, that

The argument of function burns should be of type
    { metal } / { plastic } / { wood }
instead of 
    { leather } / 'c.a

When adding a new variant to a type, it’s extremely convenient that the Opa compilers will pinpoint every place in your application code that needs to be updated to handle the new case. To do so, you can introduce explicitely the type

type material = { wood } or { plastic } or { metal }

and enforce additional checks by stating that function burns argument is of type material

function burns(material m) {

Then, if you update type material,

type material = { wood } or { plastic } or { metal } or { leather }

but not the code of function burns, the Opa compiler will complain that

File "material.opa", line 5, characters 28-107, (5:28-11:1 | 136-215)
Incomplete record pattern matching: case {leather} is missing

You too can play with the gist.

Power Rows = Extensible Records + Variants

You may have noticed that the syntax of Opa variants is the same as the syntax of records. They are indeed the same construction!

And you can use them in rows +and+ columns, hence the name of Power Rows (and later, the now abandoned nickname of Power Rangers). Let’s have a look at a real use of their features combined. Starting a new server with Opa, like in the Hello World application is written

Server.start(Server.http, 
    { title: "Hello, world", 
      page: function() { <>Hello, world</> } })

In other applications, the server can be called in other ways such as

Server.start(
    { Server.http with name : "liveroom" },
    { custom: dispatcher })

The magic behind it that in the Server.start function, we see that its second and main argument is a Server.handler defined as

type Server.handler =
   { { string text } }
or { { string title, (→ xhtml) page } }
or { { Parser.general_parser(resource) custom } }
or { { (Uri.relative → resource) dispatch } }
or { { Server.filter filter, (Uri.relative → resource) dispatch } }
or { { stringmap(resource) resources } }
or { { Server.registrable_resource register } }
or { list(Server.handler) }

Omitting the types of each element, a Server.handler can be represented in a table like this

first element second element
text
title page
custom
dispatch
filter dispatch
resources
register
nil
hd tl

Regular records are represented as rows, variants as columns. And the might of Power Rows is that you can combine both at will, while benefiting from unobtrusive strong, static typing.

Some of you might ask: But what are hd and tl in the last row and nil in the row above? Good question! They are the construct of a list in Opa, where hd is the first element of the list (or head), and tl the rest of the list (or tail). And nil matches the case of the empty list. From a user point of view, Server.handler can also be a list of handlers. With syntactic sugar:

[ handler1, handler2, handler3 ]

That’s all Folks!