问题
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.
回答1:
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.
回答2:
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.
回答3:
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, introducingOuterSys
as a holder ofTx
type and everything else (Sys
andIdentifier
as inner traits), and retainingSys
for whatever else it is doing for you - Have
Test
trait belong toOuterSys
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()
}
}
回答4:
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)
}
}
来源:https://stackoverflow.com/questions/14225582/f-bounded-quantification-through-type-member-instead-of-type-parameter