F-bounded quantification through type member instead of type parameter?

588 views Asked by At

I would like to move a type parameter to a type member.

This is the starting point which works:

trait Sys[S <: Sys[S]] {
  type Tx
  type Id <: Identifier[S#Tx]
}

trait Identifier[Tx] {
  def dispose()(implicit tx: Tx): Unit
}

trait Test[S <: Sys[S]] {
  def id: S#Id
  def dispose()(implicit tx: S#Tx) {
    id.dispose()
  }
}

What annoys me is that I'm carrying around a type parameter [S <: Sys[S]] throughout my entire libraries. So what I was thinking is this:

trait Sys {
  type S = this.type  // ?
  type Tx
  type Id <: Identifier[S#Tx]
}

trait Identifier[Tx] {
  def dispose()(implicit tx: Tx): Unit
}

trait Test[S <: Sys] {
  def id: S#Id
  def dispose()(implicit tx: S#Tx) {
    id.dispose()
  }
}

Which fails... S#Tx and S#Id became somehow detached:

error: could not find implicit value for parameter tx: _9.Tx
               id.dispose()
                         ^

Any tricks or changes that make it work?


EDIT : To clarify, I am primarily hoping to fix the type S in Sys to make it work. There are numerous problems in my case using path-dependent types. To give just one example which reflects the answers of pedrofuria and Owen:

trait Foo[S <: Sys] {
  val s: S
  def id: s.Id
  def dispose()(implicit tx: s.Tx) {
    id.dispose()
  }
}

trait Bar[S <: Sys] {
  val s: S
  def id: s.Id
  def foo: Foo[S]
  def dispose()(implicit tx: s.Tx) {
    foo.dispose()
    id.dispose()
  }
}

<console>:27: error: could not find implicit value for parameter tx: _106.s.Tx
               foo.dispose()
                          ^

Try to make that def foo: Foo[s.type] to give you an idea that this leads nowhere.

4

There are 4 answers

1
Duduk On

Although this doesn't answer your question (ensuring minimal modification of existing code), here's a thought:

Instead of Tx type being a member of Sys, and being used in Identifier, I would, as a starting point, make it a parameter of Sys, and ensure it is being used in the same way by both Id <: Identifier and S <: Sys, like this:

    trait Sys[Tx] {
        type S <: Sys[Tx]
        type Id <: Identifier[Tx]
    }

    trait Identifier[Tx] {
        def dispose()(implicit tx: Tx): Unit
    }

    trait Test[Tx, S <: Sys[Tx]] {
        def id: S#Id
        def dispose()(implicit tx: Tx) = id.dispose()
    }

This is hardly an improvement in respect to your motivation (Sys still has a type parameter), but my next step would be to convert Tx to type member. The only way I could make it work however, without using any sort of val s: S trickery (and types based on it) is to:

  • Split Sys into two traits, introducing OuterSys as a holder of Tx type and everything else (Sys and Identifier as inner traits), and retaining Sys for whatever else it is doing for you
  • Have Test trait belong to OuterSys

Here's the code:

    trait OuterSys {
        type Tx
        type S <: Sys
        type Id <: Identifier

        trait Sys {
        }

        trait Identifier {
            def dispose()(implicit tx: Tx): Unit
        }

        trait Test {
            def id: Id
            def dispose()(implicit tx: Tx) = id.dispose()
        }
    }

So although not really answering your question, or solving your problem, I was hoping it might at least give you guys some idea how to pull this through. Everything else I tried came back at me with compiler shouting for some instance of S and expecting a type based on it.


EDIT: No real need for splitting Sys:

    trait Sys {
        type Tx
        type Id <: Identifier

        trait Identifier {
            def dispose()(implicit tx: Tx): Unit
        }

        trait Test {
            def id: Id
            def dispose()(implicit tx: Tx) = id.dispose()
        }
    }

Also neglected to mention the obvious - that types depend on Sys instance, which I guess makes sense (no sharing of identifiers between systems? transactions maybe?).

No need to "test" from within Sys instance either, and no need for type S <: Sys any more (and type S = this.type in MySystem):

    object MySystem extends Sys {
        type Tx = MyTransaction
        type Id = MyIdentifier

        class MyTransaction (...)
        class MyIdentifier (...) extends Identifier {
            def dispose()(implicit tx: MySystem.Tx) {}
        }
    }

    object MyOuterTest {
    {
        def id: MySystem.Id = new MySystem.MyIdentifier(...)

        def dispose()(implicit tx: MySystem.Tx) {
            id.dispose()
        }
    }
8
Alex DiCarlo On

I have 2 versions that compile, however I'm not entirely sure either is what you are looking for in your library. (EDIT: This version is inherently flawed, see comments). Here we remove the type parameter S completely from Sys, and continue to use type projections (vs. path dependent types).

trait Sys {
  type Tx
  type Id <: Identifier[Sys#Tx]
}

trait Identifier[Tx] {
  def dispose()(implicit tx: Tx)
}

trait Test[S <: Sys] {
  def id: S#Id
  def dispose()(implicit tx: S#Tx) {
    id.dispose()(tx)
  }
}

In this version, we convert the type parameter to a type member (I'm not entirely sure this is the correct translation), and then use a combination of type refinement and type projections to assure the correct type in Test.

trait Sys {
  type S <: Sys
  type Tx
  type Id <: Identifier[S#Tx]
}

trait Identifier[Tx] {
  def dispose()(implicit tx: Tx)
}

trait Test[A <: Sys {type S = A}] {
  def id: A#Id
  def dispose()(implicit tx: A#S#Tx) {
    id.dispose()
  }
}

Also notice that we have to use A#S#Tx as our type projection for the implicit parameter, which hopefully sheds some light into why S#Id and S#Tx become "detached." In reality, they aren't detached, declaring type S = this.type makes S a singleton type, which then makes S#T a path dependent type.

To be more clear, given val a: A {type B}, a.A is shorthand for a.type#A. I.e. S#T is really this.type#T, which is also why simply declaring def dispose()(implicit tx: S#S#T) will not work, because S#S#T is a type projection, not a path dependent type as desired, as exemplified above in the answers that required a val s: S to compile.

EDIT: You can remove the parameter on Test as follows:

trait Test {
  type A <: Sys {type S = A}
  def id: A#Id
  def dispose()(implicit tx: A#S#Tx) {
    id.dispose()
  }
}

However this might require a lot of source code modification.

Regardless of if you use type parameters or type members, specifying the type won't just disappear without reworking how types work in your library. I.e., type parameters and abstract type members are equivalent, so it doesn't seem that you can get rid of the type S <: Sys[S] entirely.

EDIT2: Without using path-dependent types or something along the lines of Duduk's answer, this doesn't seem to be possible. Here is a slight modification to what I already gave that avoids passing around val s: S, however it may not be use-able in your library as it requires changing Identifier[Tx] to a type member and def id: S#Id to a val in order to expose the path dependent type:

trait Sys {self =>
  type Tx
  type Id <: Identifier {type Tx = self.Tx}
}

trait Identifier {
  type Tx
  def dispose()(implicit tx: Tx)
}

trait Test[S <: Sys] {
  val id: S#Id
  def dispose()(implicit tx: id.Tx) {
    id.dispose()(tx)
  }
}
4
pedrofurla On

Here is a version of Test that compiles:

trait Test[S <: Sys] {
  val s : S
  def id: s.Id
  def dispose()(implicit tx: s.Tx) {
    id.dispose()
  }
}

You absolutely right in saying "S#Tx and S#Id became somehow detached". You can't guarantee that in both S's they are actually the same type, as I understand.

1
Owen On

This is not so much an answer as a comment on pedrofurla's answer; which I think is correct. Let me explain why.

Scala has this funny thing where, when you write a type member of a class, it essentially creates two different names, one of which belongs to the class, and the other of which belongs to objects of that class. There is some connection between them, namely that the object member type has to be a subtype of the class member type, but in my experience you very rarely want to use this connection; most of the time you should think of them as entirely separate things.

What you really wanted to do here is package up two types so that you can give a name to the pair of them. So I would write Sys like:

trait Sys {
    type Tx
    type Id <: Identifier[Tx]
}

because that says exactly what you want to do, with no magic or fluff: create a type of objects, each of which stores two things, and those things are types (and have some constraints between them).

Then you can write Test the way pedrofurla suggestes:

trait Test {
    val s: Sys
    def id: s.Id
    def dispose()(implicit tx: s.Tx) {
        id.dispose()(tx)
    }
}

Again, only what you need and nothing extra: to create an instance of Test, you must supply a Sys, and that instance of Sys will contain the types that Test needs to work with.

In other words, sometimes just think of types as regular old values to be packaged up and passed around.


edit:

Scalability (at least in your example, there may be others I haven't thought of) should not be a problem if you again stick to exactly what you need. In your Foo/Bar example,

// This is normal; nothing unexpected.
trait Foo {
    val s: Sys
    def id: s.Id
    def dispose()(implicit tx: s.Tx) {
        id.dispose()
    }
}

trait Bar { self =>
    val s: Sys
    def id: s.Id
    // Now here's the key!
    val foo: Foo { val s: Sys { type Tx = self.s.Tx } }
    def dispose()(implicit tx: s.Tx) {
        foo.dispose()
        id.dispose()
    }
}

Here, what we really desire of our foo is that it's s.Tx is the same as our s.Tx, because what we want to do is use them interchangeably. So, we just require exactly that, and it compiles with no problems.