This has been processed with:
|ocamlorn --library stdlib.mli \ --library expr.in.ml --input expr.in.ml --lifting expr.lif.ml \ > expr.out.ml|
We assume given a type
int of integers and two functions
(they are define abstractly in
We have the following code:
|type expr = | Const of int | Add of expr * expr | Mul of expr * expr let rec eval = function | Const i -> i | Add ( u , v ) -> add (eval u) (eval v) | Mul ( u , v ) -> mul (eval u) (eval v)|
And we want to use the following representation instead:
|type binop' = | Add' | Mul' type expr' = | Const' of int | Binop' of binop' * expr' * expr'|
We define an ornament:
|type ornament oexpr : expr => expr' with | Const ( i ) => Const' ( i ) | Add ( u , v ) => Binop' ( Add' , u , v ) when u v : oexpr | Mul ( u , v ) => Binop' ( Mul' , u , v ) when u v : oexpr|
And ask for the lifting at this ornament:
|let eval' = lifting eval : oexpr -> int|
We obtain the following (complete) code:
|let rec eval' = function | Const' i -> i | Binop'(Add', u, v) -> add (eval' u) (eval' v) | Binop'(Mul', u, v) -> mul (eval' u) (eval' v)|
In fact, we need not specify the ornament relation
oexpr -> int
and have instead tell to use the ornament
oexpr whenever possible
and otherwise the identity ornament. That is, the following binding
|let eval' = lifting eval : _ with ornament * <- oexpr, @id|
will return exactly the same output in this case.
Notice that the reverse transformation works similarly, as the
ornament is actually an isomorphism:
|type ornament ioexpr : expr' => expr with | Const' ( i ) => Const ( i ) | Binop' ( Add' , u , v ) => Add ( u , v ) when u v : ioexpr | Binop' ( Mul' , u , v ) => Mul ( u , v ) when u v : ioexpr|
Then, we can lift
eval' along this transformation
into a funtion
eval'' that will happen to be equal to the function
equal we came from.
|let eval'' = lifting eval' : ioexpr -> int|
|let rec eval'' = function | Const x -> x | Add(x, x') -> add (eval'' x) (eval'' x') | Mul(x, x') -> mul (eval'' x) (eval'' x')|
By starting with a recursive copy function, we can
use the ornament to construct a function that transforms
|let rec copy = function | Const (i) -> Const i | Add ( u , v ) -> Add ( copy u , copy v ) | Mul ( u , v ) -> Mul ( copy u , copy v ) let expr2expr' = lifting copy : expr -> oexpr|
We obtained the expected result:
|let rec expr2expr' = function | Const i -> Const' i | Add(u, v) -> Binop'(Add', expr2expr' u, expr2expr' v) | Mul(u, v) -> Binop'(Mul', expr2expr' u, expr2expr' v)|
In this example, the goal of the ornamentation is more probably to factor out similar elements in the original data-structure, but the code is still duplicated for the two cases.
Indeed, the prototype try hard to maintain the original structure as much as possible by renesting patterns that were nested in the base code—but that are decomposed during the transformation.
However, we may always manually request otherwise, or rather redecomposed nested
pattern on demand. That is, from
eval', we would obtain the
|let rec eval'_1 = function | Const' i -> i | Binop'(binop, u, v) -> match binop with | Add' -> add (eval'_1 u) (eval'_1 v) | Mul' -> mul (eval'_1 u) (eval'_1 v)|
From which, we may fact factor out both branches, leading to
|let rec eval'_2 = function | Const' i -> i | Binop'(binop, u, v) -> (match binop with Add' -> add | Mul' -> mul) (eval'_2 u) (eval'_2 v)|
Again factorization could be automated, but requesting factorization should be on demand.
This is just to emphasize that lifting is just one important tool in a chain of simple transformations that we may apply when refactoring (or modifying) code. When each transformations is a pure refactoring, which preserve the semantics, as above, so is their composition.
Some examples who relift already lifted expressions process everything in <lifting> mode, since the code lifted code must be again available as input.