Sorry, I didn’t notice your reply.
If it is a sum, then you have to implement some kind of a sum type, and since RDF is a product, then you need to use some kind of a product type in the implementation. Depending on your style or need you may employ different techniques for implementing sums and products of types, the most common would be to use Algebraic Data Types, possibly generalized (later on this), but you shall keep in mind that it is not the only way. You may also opt into abstract data types. For example, when you have a sum of two concepts, (lets pick int
and float
as an example) then you can represent their sum using the sum type:
type number = Int of int | Float of float
let add x y = match x,y with
| Int x, Int y -> Int (x+y)
| Float x, Float y -> Float (x +. y)
| Float x, Int y -> Float (x +. float y)
| Int x, Float y -> Float (float x +. y)
you may also use GADT to expose extra constraints, e.g., suppose we would like the add
operation to be monomorphic, i.e., we would like to prevent from adding floats to ints and vice verse. You can’t express this with normal ADT, but GADT allows for it
type _ number = Int : int -> [`Z] number | Float : float -> [`R] number
let add : type s. s number -> s number -> s number =
fun x y -> match x,y with
| Int x, Int y -> Int (x + y)
| Float x, Float y -> Float (x +. y)
Notice, that not only the typechecker won’t allow you to add a float to int, but it will also accept the pattern as exhaustive (and even more will disallow you to write any other cases, as they do not fit the constraint).
However, ADT is not the only solution, you may also use Abstract Data Types (unfortunately abbreviated also to ADT), again OCaml provides many techniques for implementing them starting from straightforward records:
type 'a number = {
v : 'a;
add : 'a -> 'a -> 'a
}
let float v = {v; add = (+.)}
let int v = {v; add = (+)}
let add {v=x; add} {v=y} = {v=add x y; add}
going to much more heavyweight first class modules (notice type annotations):
module type Number = sig
type t
val v : t
val add : t -> t -> t
end
type 'a number = (module Number with type t = 'a)
let add : type t. t number -> t number -> t number =
fun (module X : Number with type t = t)
(module Y : Number with type t = t) ->
(module struct
include X
let v = X.add X.v X.v
end)
and finally going to object and classes:
class type ['a] number = object
method value : 'a
method add : 'a number -> 'a number -> 'a number
end
class r x : [float] number = object
method value = x
method add x y = new r (x#value +. y#value)
end
class z x : [int] number = object
method value = x
method add x y = new z (x#value + y#value)
end
Now, after this showcase, let’s get back to your example. But keep in mind, that I’m not trying to show you the way, but instead, I’m showing you different ways, so that you can build your own intution and pick the right way. In other words, I’m trying to tell you how to implement instead of what to implement. Anyway, back on track 
So you have a nice interface that is easy to use correctly and impossible to use incorrectly. Now the easy part - you need to implement it. When we choose between algebraic representation and abstract representation we have to consider our design constraints and fit them into the expression problem (described better here). If we will more likely add more operations then we have to stick with algebraic data types, if we more likely will add more entities, but the behavior is fixed, then we need to use abstract data types. If we are going to add both, we need to use the Tagless-final approach.
Fortunately, our case is simple, since we have a fixed set of objects, that we’re not going to extend, namely iri
, node
, and literal
. Hence, we will stick to algebraic data types.
The next question is whether we should use plain ADT for our elt
ty[e, and keep the type parameter phantom, or use constrained GADT. The first option is usually easy to implement, but you can’t rely on the type inference to check that your code is correct. Basically, the type system will admit whatever you will tell it about your phantom types. And since constraints are erased from ADT the exhaustiveness check will be overly pessimistic so you will be forced to spill quite a few assert false
s around your code. It doesn’t sound like a lot of fun, but sometimes, this is the only way to implement the signature (GADT power is still limited), so let’s look a this implementation:
type _ elt = (* type parameter is not used on rhs - thus it is a phantom *)
| I of iri
| N of node
| L of literal
(* we don't want the constraint to escape to the rdf type, so we may use
GADT here to implement an existential. We can use first class modules,
objects, records with universal quantifiers, but GADT is the least
verbose here *)
type t = Rdf : _ elt * _ elt -> t
(* create is easy, but it loses the constraint that we've specified in
the signature *)
let create x y = Rdf (x,y)
(* hence the typechecker expects unexpectable *)
let show (Rdf (x,y)) = match x,y with
| I x, I y -> x ^ y
| N x, N y -> x ^ y
| I x, N y -> x ^ y
| N x, I y -> x ^ y
| I x, L y -> x ^ y
| N x, L y -> x ^ y
(* nothing else is posible, but the typechecker doesn't know this *)
| _ -> assert false
Fortunately, we can use GADT to keep our constraints,
type 'a elt = (* the constraint is propagated to the branches *)
| I : iri -> [`iri] elt
| N : node -> [`node] elt
| L : literal -> [`literal] elt
(* and kept alive in our existential *)
type t = Rdf : [< `iri | `node] elt * [<`iri | `node | `literal] elt -> t
(* create is still the same easy *)
let create x y = Rdf (x,y)
(* but show is now exhaustive *)
let show (Rdf (x,y)) = match x,y with
| I x, I y -> x ^ y
| N x, N y -> x ^ y
| I x, N y -> x ^ y
| N x, I y -> x ^ y
| I x, L y -> x ^ y
| N x, L y -> x ^ y
(* and we can't even add an impossible combination here *)
To summarize, OCaml is a very versatile language and provides many different techniques which you can employ to model your problem as tight as possible. Mastering all the techniques takes time, an investment that will be returned once you will start to apply them in the real world scenarios. A properly chosen abstraction will save you a lot of time in the software lifecycle.