Merging unfold, fold and map transformations
adt.js (9.2Kb)
Algebraic data types are types like you can declare in Haskell with data
declarations.
If you are not familiar with Haskell, you can try the
Wikipedia page about algebraic data types.
I have written the library in Javascript 1.8, which means that as of this writing it only runs in Firefox 3.0. I have chosen to keep it 1.8, because the code is a lot cleaner, thanks to the new expression closures. But there is nothing that cannot be made to work in ECMAScript edition 3. And I have no doubt that the same thing could be done in Python or Ruby.
A first example:
Now you can create point objects like this: Pt(1, 2, Red)
. Note that you don't need new
,
alltough you could; Pt
is a real constructor. All of the following works as expected:
You can also define recursive types. F.e. Peano numbers:
With Peano numbers you would represent f.e. 2 as S(S(Z))
. Note that for the recursion,
we use the first argument of the declaration function.
Now lets take a look at type parameters. We take lists as example:
Type parameters become extra arguments of the declaration function, after the recursion argument.
Finally, let's take a look at a more complete example, a simplified XML data structure:
There are two things to note here: one is that we instantiate the parameterized types List
and Attrs
, and the second is that adding names to the constructor arguments makes the type easier to understand.
With unfold you can easily generate algebraic data structures from other data. The following example shows how to generate a decreasing list of numbers.
Unfolds do not use the actual constructors, but stand-ins that perform recursion at the right places.
In this example c.Cons
recursively calls the unfold function for it's second argument,
and then calls the Cons
constructor with the result. Let's try it out:
Fold is the reverse of unfold. It destructs data into a return value. Here is an example that multiplies a list of numbers.
You can understand a fold as replacing the constructors with the given functions. Let's test this code:
You may have noticed that with combining prod
and counter
we have created the
factorial function. However, it is quite wasteful as it builds up a list which it then destroys again.
But this adt library has a solution for that, it allows you to merge functions that are defined over the same
data type:
Now we have a factorial function that was neatly defined using the structure of List, but no list is created.
c.Cons
instead of calling the Cons
constructor, now directly calls the Cons
function from prod
.
With map
you provide a function for each type parameter. Here are 2 examples using the XML data types:
The first adds a prefix to each name, leaving values alone (using the id
function), and the second strips whitespace from
values, leaving names alone. For a more complete example, let's write a serialization function for the XML data:
Now if we wanted to add prefixes, normalize space and then serialize the xml, we could write:
But then we would traverse the data structure 3 times. We would like to merge these functions so that the data structure is traversed only once. (When this is done in the compiler it is called deforestation or fusion.)
In general you can merge one fold, one unfold and any number of maps, as long as they are defined on the same data type.
All these unfolds, folds, maps and any merged combination of them are special cases of transformers. A transformer declaration has 4 parts: a generator (unfold), transformation of constructors (fold), transformation of type parameters (map) and transformation of non-algebraic types. The last tree are best explained with an example:
Then the following are equivalent:
Ie. each constructor is replaced with f
, each value in a type parameter position is wrapped with g
,
and other (non-algebraic) values are wrapped with h
.
Derived properties are like derived classes in Haskell, they apply to every algebraic data object that is created.
The adt library already contains clone
, source
, equals
, size
and
items
. Some examples for the previously defined xml
object:
But you can also add your own derived properties. Here is how you would define items
:
To see if even advanced Haskell code would work in JavaScript, I took a shot at Data types à la carte by Wouter Swierstra. It addresses the Expression Problem:
The goal is to define a data type by cases, where one can add new cases to the data type and new functions over the data type, without recompiling existing code, and while retaining static type safety.
Of course, that last property is lost in the translation to JavaScript, but otherwise this code works.
First the initial data type, expressions with values and addition.
Next, we create an evaluation function with fold
.
Now we can add multiplication to the expression type.
And finally we add a render
function.