I start with some explanation of why solving this problem would bring great good to all.
And then I put forward a puzzle that needs solving.
A little motivational background
There is an incredibly useful application of Match Types to bring together different code bases under the same API without loosing efficiency! This is especially useful for different implementations of the same standard. For example with Banana-RDF we have a library that unites 2 Java implementations of W3C standards and one JS implementation written by Tim Berners-Lee. And we should be able to add new ones, when needed.
The traditional Java way would be to write a unified interface and then wrapping every object with that interface to make that the standard mode of interaction, as done in Apache’s Common RDF. See their Triple Interface which requires every Triple in a store (and there can be millions) to be wrapped in a new object that has to wrap a subject, a relation and an object, creating thus 4 extra objects in the worst case.
With banana-rdf on the other hand, one can write your code to one unified interface but using directly the underlying objects of the library one is interested in without creating any new objects! This can be made secure with opaque types
. Then by changing one line of code one can switch between the different underlying implementations with no loss of efficency. The code for each implementation is locked in an Ops[R] type that is passed implicitly as a given
. A good example of such code is GraphTest for which we then have one line implementations for Jena and similarly one line implementations for IBM’s RDFlib.
I recently used the same trick to write a little abstraction for Http Messages that allowed me to abstract between Akka and Http4s to help me reduce the amount of test codes for a Signing HTTP Messages implementation, an IETF spec which is in last call.
One could use the same trick to help abstract between all the various Uri implementations, from Akka’s, to Http4s, lemonlabs Uri, the Java Uri class and even the up and coming cats Uri class, or even the usually much less rich URI classes from the RDF frameworks, not counting all the JavaScript Uri classes. See issue 7 on typelevel/cats-uri for more details.
I can see this being extremely useful in many projects where different teams implemented the same spec, and developers would rather not be tied to any one of them, but switch between them as needed easily.
But I have a little problem, which is making things a bit less nice than they should be…
Inheritance Problem of Match Types
How do I get inheritance to work? The RDF library has a lot of types (see RDF.scala), and I can’t work out how to get the inheritance to work correctly. I can perhaps solve the problem with Conversion
types, but that really looks ugly, and the complexity is growing very fast.
So here is a simplified version of the code I am using:
trait RDF:
rdf =>
type R = rdf.type
type rNode <: Matchable
type Node <: rNode
type URI <: Node
type BNode <: Node
given rops: ROps[R]
end RDF
object RDF:
type rNode[R <: RDF] <: Matchable =
R match
case GetRelNode[n] => n
type Node[R <: RDF] =
R match
case GetNode[n] => n
type BNode[R <: RDF] = R match
case GetBNode[bn] => bn
type rURI[R <: RDF] = R match
case GetRelURI[ru] => ru
type URI[R <: RDF] = R match
case GetURI[u] => u
// for info about Getxxx types see https://github.com/lampepfl/dotty/issues/13416
private type GetRelURI[U] = RDF { type rURI = U }
private type GetURI[U] = RDF { type URI = U }
private type GetRelNode[N <: Matchable] = RDF { type rNode = N }
private type GetNode[N] = RDF { type Node = N }
private type GetBNode[N] = RDF { type BNode = N }
end RDF
trait ROps[R <: RDF]:
def nodeVal(node: RDF.Node[R]): Int
object SomeObject:
def calculate[R <: RDF](node: RDF.Node[R])(
using ops: ROps[R]
): Int =
ops.nodeVal(node)
object OtherObject:
def uriParse[R <: RDF](uri: RDF.URI[R])(using
ops: ROps[R]
): String =
ops.nodeVal(uri).toString()
The problem is with the last line. The ops.nodeVal
function expects a RDF.Node[R]
. We give it an RDF.URI[R]
which should be a type that is a subtype of it. But it complains.
I have tried quite a few things to see how to get around this, the last one being described here but with no luck…
This can be seen in this Scastie: