Evgeny
Evgeny

Reputation: 103

Scala type parameter as injective mapping

Have some type T and abstract class X[T], and what the main point is, for each concrete type T, if defined, there is only one subclass of X[T], for instance, IntX extends X[Int], which is the only subclass for X[T] with T = Int. That is, theoretically we have an injective mapping T -> X[T] for some set of types.

Let's have a look on two simple definitions:

trait Context[T] {
    type XType <: X[T]
}

abstract class X[T] extends Context[T] {
    def plus1(that: XType): XType = ??? /* doesn't matter */
    def plus2(that: XType): XType = that plus1 this
    def sum(x1: XType, x2: XType): XType = x1.plus1(x2)
}

Here we see our X[T] with some methods. To have correct final types in concrete inherited subclasses, I use XType as type of subclass inherited from X[T]. For instance, like this one:

trait IntContext extends Context[Int] {
    type XType = IntX
}

class IntX extends X[Int] with IntContext

And then method IntX.plus1 takes IntX and returns IntX, not X[Int], so this is a brief explanation of rather abstract example. Context is used to contain all information about types and fabric constructors, related to each used type T. Well, there is more meaningful example of Context, just to understand things correctly:

trait Context[V <: ArithmType[V]] { /* V such as Int, Double */
    type Point <: AbstractPoint[V]
    type Line  <: AbstractLine[V]
    type Rect  <: AbstractRect[V]
    ...
    def newPoint(x: V, y: V):          Point
    def newLine(v1: Point, v2: Point): Line
    def newRect(p: Point, w: V, h: V): Rect
    ...
    def pointCompanion: AbstractPoint.Companion[V]
    def lineCompanion:  AbstractLine.Companion[V]
    def rectCompanion:  AbstractRect.Companion[V]
    ...
}

The problem is:
The code with X[T] will not compile. Of course, if we have a look on two last methods, we will get the following errors:

Type mismatch, expected: that.XType, actual: X[T]
Type mismatch, expected: x1.XType, actual: X.this.XType

We see that compiler treats own type of each instance of XType variable as different one from each other. And this is right, of course, but the thing compiler doesn't know is the injectivity of our inheritance: for fixed type T all XType type values are the same.

How could I implement such a logic to overpass this?


I've designed one solution, but it's rather dirty. Rewrite the code:

trait Context[T] {
    type XType <: X[T]
    implicit def cast(x: X[T]): XType = x.asInstanceOf(XType)
}

abstract class X[T] extends Context[T] {
    def plus1(that: XType): XType = ??? /* doesn't matter */
    def plus2(that: XType): XType = that plus1 that.cast(this)
    def sum(x1: XType, x2: XType): XType = x1 plus1 x1.cast(x2)
}

Without implicit casting the methods will be like:

def plus2(that: XType): XType = cast(that plus1 that.cast(this))
def sum(x1: XType, x2: XType): XType = cast(x1 plus1 x1.cast(x2))

asInstanceOf-casting will not fail as we know about our constraint on injectivity. One can use pattern matching, but this are details.

The main disadvantage of this solution is the need of class code refactoring: we put some cluttering casts in out business logic parts.

Does this solution have the right to be used? What ideas do You have?


Edit: Is there a way to use Aux technique in this case?

Upvotes: 1

Views: 62

Answers (1)

Andrey Tyukin
Andrey Tyukin

Reputation: 44918

If for each T there is always only one concrete subclass Repr <: X[T], then this class Repr will itself know that every other X[T] must be Repr. So, just give the type Repr as an argument to X, so it can use it in all plusXYZ-method declarations:

trait Context[T, Repr <: X[T, Repr]]

abstract class X[T, Repr <: X[T, Repr]] extends Context[T, Repr] {
    def plus1(that: Repr): Repr = ??? /* doesn't matter */
    def plus2(that: Repr): Repr = that plus1 that
    def sum(x1: Repr, x2: Repr): Repr = x1 plus1 x2
}

class IntX extends X[Int, IntX]

While this works, a word of warning: all those circular f-bounded-polymorphism stunts tend to become rather nasty rather quickly. Typeclasses tend to compose much nicer.

And by the way: I'm not sure what the function of Context in the above code snippet is. It doesn't seem to do anything.

Upvotes: 1

Related Questions