F-bounded quantification through type member instead of type parameter?
Asked Answered
M

4

3

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.

Mannos answered 8/1, 2013 at 23:4 Comment(3)
What about using object someSys extends Sys[someSys] { ... }?Huldahuldah
Oh, think I see you point now.Huldahuldah
@Huldahuldah -- yes that's what I was doing until now, object DurableSys extends Sys[DurableSys] etc. This does work. So the question is, can I get rid of the type parameter on Sys which really pollutes my sources.Mannos
H
2

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.

Huldahuldah answered 8/1, 2013 at 23:57 Comment(4)
There are two problems with this is. (1) No other object will be able to call into Test's dispose, because it won't have a transaction with the path-dependent contraints (s.Tx). (2) Then I need pass around an embodiment of S all around my library. I tried to do at a long time ago, it was so PITA, that I was very happy when I got rid of the s value.Mannos
Can't the pass around be done with single trait extended "all around" ?Huldahuldah
There is only one instance of S. It would mean to change each and every constructor of every possible class in the library, including implicitly looked up types such as serializers. That's just not feasible. (besides: I like the path-dependent type because it is safer than the type projection, but all my experiences with path-dependent types so far just caused me big headache.) So I was hoping the surgery could be just done inside the Sys trait...Mannos
I am trying to achieve what you want (here doing experiments...). Pretty much the type projection limitation is exactly that A#X and A#Y can't guarantee both A's are the same.Huldahuldah
B
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.

Bilge answered 9/1, 2013 at 0:10 Comment(1)
The problems occur when you zoom out. I have edited the question to demonstrate the scalability problem.Mannos
A
1

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()
        }
    }
Avert answered 9/1, 2013 at 1:55 Comment(1)
Thanks Duduk. There is some wisdom in the wrapping inside one outer trait. First of all, things like Test could not be within the system trait itself, as this is a modular system, and test might for example be a data structure where it must be possible to parametrise it with any system (in-memory, persisted, etc.); On the other hand, I have used the approach with the outer trait to wrap multiple related structures together under one common type parameter S, so this is not so dissimilar.Mannos
B
1

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)
  }
}
Brookbrooke answered 9/1, 2013 at 9:33 Comment(8)
Thank you very much for these two versions. The first one doesn't seem to be able to implement any concrete system (?), e.g. trait TxnX; trait IdX extends Identifier[TxnX] { def dispose()(implicit tx: TxnX) {}}; trait SysX extends Sys { type Tx = TxnX; trait Id = IdX } -- the SysX doesn't compile saying that overriding type Id in trait Sys with bounds <: Identifier[Sys#Tx] has incompatible type. Not sure yet, why, because this would be a very simple solution (tried adding variance to the Tx parameter of Identifier, didn't help).Mannos
The second version unfortunately defeats the purpose of making passing the system type parameter less verbose (S <: Sys[S] versus S1 <: Sys { type S = S1 }) :-(Mannos
Ah yes, the first version is virtually useless. (I wrote it past 4 in the morning on no sleep and didn't really think past compilation).Brookbrooke
Which is sad, because the first version is the simplest; I'm still not sure why the subclassing of Sys doesn't work there. I have a vague feeling it is because type members do not allow for variance annotations, but not sure...Mannos
Yea, I'm looking into it more, I also added an edit for the second version in the post.Brookbrooke
Ok, but don't look too long :) I'm not sure it is possible what I'm trying to do (and I have a working version, so I was just curious if people had other ideas how to maybe solve it).Mannos
Yea I can't seem to figure out how to convert F-Bound type parameters to type members, I asked a question here: #14244760 which is very similar to this one, albeit in a much simplified formBrookbrooke
The above question was answered, but it didn't really get me anywhere for your specific question, so I added another version above that I confirmed works with your example in the comment. It's fairly similar to Duduk's answer, however instead it uses the route he avoided, namely using path dependent types on id. Good luck!Brookbrooke

© 2022 - 2024 — McMap. All rights reserved.