Skip to content
This repository has been archived by the owner on Jun 5, 2023. It is now read-only.

class PG[val t: RDF](node: t.Node, gr: t.Graph)? #133

Closed
bblfish opened this issue Aug 24, 2020 · 8 comments
Closed

class PG[val t: RDF](node: t.Node, gr: t.Graph)? #133

bblfish opened this issue Aug 24, 2020 · 8 comments

Comments

@bblfish
Copy link

bblfish commented Aug 24, 2020

Porting banana-rdf to Dotty I stumbled across the removal of type projections which we use a lot and I think wisely.
We have an RDF trait which specifies all the main types declared by the W3C specs.

trait RDF {
   type Graph
   type Triple
   type Node
...
}

This allowed us to abstract the various Java (Jena from HP/Apache and RDF4J from IBM) and Javascript implementations by defining functions between the types and mapping those methods for each implementation in an RDFOps[Rdf]. This allows one to write libraries in an implementation agnostic way by implicitly importing an ops. One can then write code once and change implementation with one line.

We could thus create classes like the following:

class PointedGraph[Rdf<:RDF](pointer: Rdf#Node, graph: Rdf#Graph)

With the removal of projections in dotty I tried this instead:

class PointedGraph(using val rdf: RDF)(pointer: rdf.Node, graph: rdf.Graph)

which is quite close. The implicit has to come first to be used to select the types in the arguments. Apart from not being usable in case classes it also forces the following usage

new PointedGraph()(p, graph)

Note the extra parenthesis that can't be dropped even using a companion object's apply method.
So this made me think that something along the lines of the following could be done:

case class PointedGraph[val rdf: RDF](pointer: rdf.Node, graph: rdf.Graph)

This makes sense since the role of rdf is to give us access to the dependent types and I could then imagine that type inferencing could allow one to write val pg = PointedGraph(p,g) giving us the same ease of use as before.

There is perhaps another reason to think of doing this, which I came across attempting to rewrite the library. I translated the PointedGraphs class to:

class PointedGraphs(using val ops: RDFOps)(
  val nodes: Iterable[ops.Rdf.Node], 
  val graph: ops.Rdf.Graph
) extends Iterable[PointedGraph] {
  import ops.Rdf

But then this fails in a method that returns a PointedGraph, because the dependent type is no longer explicitly tracked. I tried fixing it by writing the following in my IDE

def /(p: Rdf.URI): PointedGraphs(ops) = { //<-- a dependent argument type
    val ns: Iterable[Rdf.Node] = this flatMap { (pointed: PointedGraph) =>
      import pointed.pointer
      ops.getObjects(graph, pointer, p)
    }
    new PointedGraphs()(ns, graph)  // <-- needs an extra () for the implicit
  }

Doing that lead me to discover discover the very interesting Dependent Argument Types PR. (ie. it's an intuitive thing to do).

Following on the proposal here, perhaps the following may be more intuitive:

def /(p: Rdf.URI):  PointedGraphs[ops] 

This is not a feature request, rather a suggestion inquiry, as I don't yet feel
I have a complete grasp of the whole of dotty to be sure I have not missed
some important feature.

@bblfish bblfish changed the title enabling something like class PG[val t: RDF](node: t.Node, gr: t.Graph) class PG[val t: RDF](node: t.Node, gr: t.Graph) Aug 24, 2020
@bblfish bblfish changed the title class PG[val t: RDF](node: t.Node, gr: t.Graph) class PG[val t: RDF](node: t.Node, gr: t.Graph)? Aug 24, 2020
@smarter
Copy link
Member

smarter commented Aug 24, 2020

But then this fails in a method that returns a PointedGraph, because the dependent type is no longer explicitly tracked

You can work around that by adding a type parameter to PointedGraph:

class PointedGraph[T <: RDF & Singleton](using val rdf: T)(g: rdf.Graph) {
  def foo(): PointedGraph[rdf.type] =
    new PointedGraph()(g)
}

@smarter
Copy link
Member

smarter commented Aug 24, 2020

Also you can workaround some of the weirdness with having to add empty parameter lists to constructor calls by adding an apply method in the companion:

object PointedGraph {
  // Allows writing `PointedGraph(g)` instead of `new PointedGraph()(g)`
  def apply[T <: RDF & Singleton](using val rdf: T)(g: rdf.Graph): PointedGraph[T] =
    new PointedGraph()(g)
}

@smarter
Copy link
Member

smarter commented Aug 24, 2020

Also you can workaround some of the weirdness with having to add empty parameter lists to constructor calls by adding an apply method in the companion

I've reopened scala/scala3#2576 to discuss this issue further.

@Sciss
Copy link

Sciss commented Aug 24, 2020

Henry, you seem to have similar issue than me in my project Lucre, similar use case and structure indeed, I'm happy to see I'm not alone here. I will keep following to see how you manage to work around the removal of type projections.

@bblfish
Copy link
Author

bblfish commented Aug 24, 2020

In order to better test my port of banana-rdf I opened a small git repo where I can play with small parts of the translation called banana-play. I have gotten to a good start there. PointedGraph is somewhat more complicated than what we have now. But things get very tricky when one inherits traits with path dependent types. See the need for rdf, rdf2 and rdf3 in PrefixBuilder.

I discovered that feature request 14 describes quite well the background to the problem.

@bblfish
Copy link
Author

bblfish commented Aug 27, 2021

How did you solve your problem with Lucre @Sciss ? (I see you moved to Scala3)

@Sciss
Copy link

Sciss commented Sep 1, 2021

I swapped the position of the outer type, Sys in my case, and the most important of the type member, Txn in my case, then redraw the remaining type members, such as Vr and Id, to take a type constructor argument T <: Txn[T]] instead of a type projection S#Tx. It was quite a rewrite.

Before:

trait Sys[S <: Sys[S]] { // "dominant type"
  type Tx <: Txn[S]
  type Vr[A] <: Var[S#Tx, A]
  type Id <: Ident[S#Tx]
}

trait Txn[S <: Sys[S]] {
  def newId(): S#Id
  def newVar[A](id: S#Id, init: A): S#Var[A]
}

trait Ident[Tx]

trait Use[S <: Sys[S]] {
  def foo()(implicit tx: S#Tx): Any
}

after

trait Sys

trait Txn[T <: Txn[T]] { // "dominant type"
  type Id <: Ident[T]
  def newId(): Id
}

trait Ident[T <: Txn[T]] {
  def newVar[A](init: A)(implicit tx: T): Var[T, A]

  /** Ensures that the identifier is actually valid in the current transaction. */
  def ! (implicit tx: T): tx.Id
}

trait Use[T <: Txn[T]] {
  def foo()(implicit tx: T): Any
}

In the end this looks a bit simpler on the use site, the ubiquitous S#Tx has been replaced by T, and other cases of type projections. The most difficult thing to understand was to get the identifiers working, see the method def ! which is kind of the trick I found. This turns an Ident[T] into tx.Id. The implementation is usually just a return this operation:

class IdImpl extends Ident[Plain] {
  def !(implicit tx: Plain): Id = this
}

@bblfish
Copy link
Author

bblfish commented Sep 1, 2021

@Sciss thanks. I will study that. I put up a thread here to help collect ideas on how to deal with this. There are some good ones that have popped up in the last three days. see dotty discussions 12527

@lampepfl lampepfl locked and limited conversation to collaborators Jun 5, 2023
@ckipp01 ckipp01 converted this issue into a discussion Jun 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants