Context
I want to model the routes of a web application such that it satisfies the following requirements:
- Can enforce complete definitions
- Can create incomplete definitions
- Can check if an incomplete definition "matches" (i.e. is contained in) a complete definition.
As an example to work with:
type root =
| Fruit of fruit
| Veggie of veggie
and fruit =
| Apple of apple
| Banana of banana
and veggie =
| Carrot of carrot
and apple = { diameter: int; cultivar: string; }
and banana = { length: int }
and carrot = { length: int; color: [`Orange | `Purple] }
With this, we can easily create and enforce complete definitions:
let complete = Fruit (Apple { diameter = 8; cultivar = "Golden Delicious" })
But cannot create incomplete definitions
let incomplete = Fruit Apple
^^^^^
Error: The constructor Apple expects 1 argument(s),
but is applied here to 0 argument(s)
And therefore also cannot match incomplete and complete definitions, but we can at least verbosely implement a partial equality function that ignores the parameters:
let equal a b = match a, b with
| Fruit (Apple _), Fruit (Apple _ ) -> true
| Fruit (Apple _), _ -> false
| Fruit (Banana _), Fruit (Banana _ ) -> true
| Fruit (Banana _), _ -> false
| Veggie (Carrot _) , Veggie (Carrot _) -> true
| Veggie (Carrot _) , _ -> false
A vague idea towards a possible solution
So I had this idea of using a tree of GADTs and a heterogeneous list to make the definitions more flexible by having the routes defined as lists, e.g.:
let route = [Fruit; Apple; { diameter = 6; cultivar = "Granny Smith" }]
they could then be used with pattern matching and recursion to more easily destructure and compare them.
Unfortunately implementing this isn't quite so easy. This is what I have so far:
type _ root =
| Fruit : _ fruit root
| Veggie : _ veggie root
and _ fruit =
| Apple : apple fruit
| Banana : banana fruit
and _ veggie =
| Carrot : carrot veggie
and apple = { diameter: int; cultivar: string; }
and banana = { length: int }
and carrot = { length: int; color: [`Orange | `Purple] }
type 'a t =
| [] : _ root t
| ( :: ) : 'b * 'a t -> 'b t
Two problems that I see here:
'bisn't constrained by'a, so anything can be put into the list, as long as it starts with aroot t, and there probably isn't a way to recover the type of the elements either. I think this would require Higher-Kinded Types, but maybe there's a way around that?- Even if I was able to solve that, I'm not sure how I'd be able to terminate it. Perhaps the params could be made into GADTs too, and terminate with
unit.
It is possible to have heterogeneous lists for which the type of each element depends on the previous element and impose the constraint on the following type. The core idea is to realize that each element need to define in which context it is allowed and which context, and it is then a matter of chaining matching context:
Here the type
('a,'b) tdescribes a heterogeneous list which start at the context type'aand stop at the context type'b. And it the type definition of('a,'b) elementwhich determine which transitions is allowed.In your case, the element type could be defined as something like
It is important to notice that the module
Tagonly provides type level tags(indices) that are not associated to any value.With this definition:
is a
(Tag.root,Tag.fruit) element: the element is only allowed to the top and requires that the following element is allowed in theTag.fruitcontext. A valid next element would then bewhich is a
(Tag.root,Tag.apple) tpath. Finally, it is possible to close a path with theEndconstructor once we are in a context that maps to concrete type:And this construction is still static enough that it is generally possible to recover enough type information for handling partial paths at the price of some redundancy: