Power Rows (the datatype)

When I conceived Opa, one of the key features was the support of Power Rows. Power Rows are a powerful, statically-typed, extension of records and it’s one of the features that makes static programming as fun as dynamic programming.

This is the first part of a two parts article. Part 2 is here.

(An early version of this post mentioned the internal name of Power Rangers which were given to them as a joke, as it sounded close to Power Rows. We’re back to Rows, the original name of the construct in Semantics of PL.)

The problem

Dynamic programming is cool

For instance, in JavaScript, we can write:

> a = { name: "John", surname: "Doe" }
[object Object]

to define a record (indeed an object) with two fields. We can access a field easily:

> a.name
John

and add new fields:

> a.age = 33
33
> a.age
33

Even better, we can define a function that accesses a given field for any value, no matter what the other fields are:

> b = {name: "Mary"}
[object Object]
> f = function(x) { return x.name }
function (x) { return x.name; }
> f(a)
John
> f(b)
Mary

This is the flexibility that we all love about dynamic programming. And although I have been coding mostly in OCaml since 2001, I also did quite big amounts of Python, JavaScript, PHP and loved this part of dynamic programming.

Static programming is boring

Compared to JavaScript, records in OCaml suck. Let’s have a look:

# let a = { name = "John"; surname = "Doe" };;
Error: Unbound record field label name

That’s a good start! Not mentioning the terrible syntax, we have to define types manually.

# type person = { name : string; surname: string };;
type person = { name : string; surname : string; }
# let a = { name = "John"; surname = "Doe" };;
val a : person = {name = "John"; surname = "Doe"}

What if we want to add a field? Well, we can’t. The best we can do is to create a new type that binds together a person and its age.

# type person_with_age = { person : person; age : int };;   
type person_with_age = { person : person; age : int; } 
# let aa = { person = a; age = 33 };;
val aa : person_with_age = {person = {name = "John"; surname = "Doe"}; age = 33}

But then, it becomes cumbersome to use. What if we want to write a function that takes a person or a person_with_age? Well, we can’t. We can write a sum-type with a constructor:

# type any_person = P of person | PA of person_with_age;;
type any_person = P of person | PA of person_with_age

and use pattern-matching to deconstruct an any_person:

# let get_name = function
  | P p -> p.name
  | PA pa -> pa.person.name;;
val get_name : any_person -> string = <fun>

But then, our program has to use any_person all over. Of course, OCaml has objects which you can use instead, but they are much harder to construct and handle.

So, all the fun of programming, the flexibility of JavaScript is gone. If you read through here, you might wonder why I was writing OCaml. I am not a masochist and chose the best tool for the work I had to do.

But strong static types rock

When writing complex programs, strong static typing (with inference, like OCaml does) is extremely handy to write high-quality code, and spend a lot less time debugging. Especially when the size and complexity of programs grow.

Back to JavaScript, we get undefined when accessing a field which is not present.

> f({})
undefined

That’s great. But computation continues… So I could have:

> "Hi " + f({})
Hi undefined

And when, again, my program gets bigger, it becomes harder and harder to find the bugs timely (ideally, just when you write them). And it becomes potentially a nightmare to iron out some bugs. In OCaml,

# type another = { another: string };;
type another = { another : string; }
# let v = { another = "foo" };;
val v : another = {another = "foo"}
# get_name(v);;
Error: This expression has type another but an expression was expected of type any_person

Not only we get the error during compilation (i.e. before we run, perfect timing), but the error allows us to locate the bug easily.

When we learn we can trust the compiler, we have the satisfaction to write high-quality code when it compiles.

The solution

So, for a few years, I dreamed about what we now have built in Opa: Power Rows.

Power Rows are the best of both world. The flexibility of records from dynamic programming languages, and the type checker that goes with it.

This is how it works when you write code (there is no REPL in Opa yet, you have to print return values when you run your app).

a = { name: "John", surname: "Doe" }

a.name

Same JavaScript syntax. No type declaration beforehand ala OCaml.

It’s slightly different to add a field:

a = { a extend age: 33 }

a.age

but it’s much better to enforce the extend semantics so we can check for instance that a.nam = "Jack" is not a typo! But again, no hard declaration. It just works.

b = {name: "Mary"}

// there is no return in Opa, the last statement is the return
f = function(x) { x.name }

f(a)
f(b)

Gives us the expected result. It’s as simple as JavaScript and it just works. But unlike JavaScript, we know we write good code, when the compiler straightforwardly rejects, at compile time, the following mistake:

c = { foo: "bar" }
f(c)

with the error message (again, at compile time, before runtime):

Type Conflict
  (8:17-8:26)         { name: 'a; 'r.a }
  (12:5-12:18)        { foo: string } / 'c.a

The argument of function f should be of type
{ name: 'a; 'r.a }
instead of 
{ foo: string } / 'c.a

See, the best of both worlds! Gist is here.

There’s more to it

Power Rows might doesn’t stop here. More is written in a subsequent blog post. Meanwhile, you can play with the Opa Power Rows if you feel like it.

I also didn’t plan to explain how our type checker works in this post, but let me tell you it was a very complex task. If you are doing language research, there is no paper yet but the OCaml implementation is hidden in here.

Read on to Part 2