Why is this an invalid use of Scala's abstract

2019-08-10 16:34发布

问题:

I have this code:

class A extends Testable { type Self <: A }

class B extends A { type Self <: B }

trait Testable {
    type Self
    def test[T <: Self] = {}
}

object Main {
    val h = new A
    // this throws an error
    h.test[B]
}

And my error is:

error: type arguments [B] do not conform to method test's type parameter bounds [T <: Main.h.Self]
    h.test[B]

On this question, it was said that this was due to path dependent types. Can anyone figure out how to have T <: Self, without having the path-dependent types problem?

Any help would be appreciated.

回答1:

Your code need to be looks like:

// --- fictional scala syntax ---
class A extends Testable { type Self = A }
class B extends A { override type Self = B }

But it is imposible in current version of scala.

I would propose little bit long way (not longer than using path dependent types but another), and it conforms your requirements.

a) Use Type-class pattern for test method;

b) Use implicit parameters for conforms type relations.

Class hierarchy:

trait Testable
class A extends Testable
class B extends A

Conforms trait:

trait Conforms[X, Y]

Testable Type-class:

object TestableTypeClass {
  implicit def testMethod[T <: Testable](testable : T) = new {
    def test[X](implicit ev : Conforms[X, T]) = {}
  }
}

test method type parameter conditions in companion objects:

object A {
  // P <: A is your conditon (Self <: A) for class A
  implicit def r[P <: A] = new Conforms[P , A] {}
}
object B {
  // P <: B is your conditon (Self <: B) for class B
  implicit def r[P <: B] = new Conforms[P , B] {}
}

Tests:

import TestableTypeClass._

val a = new A
a.test[A]    // - Ok
a.test[B]    // - Ok

val b = new B
// b.test[A] // - did not compile
b.test[B]    // - Ok

UPDATE:

1) It is possible to collect all implicits in one object, and in this case object with implicits need to import (it is not needed before by rules of implicit scope in companion object):

object ImplicitContainer {
  implicit def r1[P <: A] = new Conforms[P , A] {}
  implicit def r2[P <: B] = new Conforms[P , B] {}
} 

and using:

import TestableTypeClass._
import ImplicitContainer._

val a = new A
a.test[A]
a.test[B]   

2,3) trait Conforms defined for 2 type parameter X & Y

X - used for future type constraint (and this constraint come from parametric method)

Y - used for determine the type for which will be define type constraint

implicit parameter choise by Comforms instance type, and idea of this design is playing with combinations X & Y

in Type-class TestableTypeClass type Y captured by implicit conversion from Testable to anonimous class with test method, and type X captured in test method call.

And a main feature is invariance of Conforms trait, this is why implicits is not ambiguous and correctly manage bound rules.

And for better understanding, one more example with more strict rules:

//...
class C extends B

object ImplicitContainer {
  implicit def r1[P <: A] = new Conforms[P , A] {}
  implicit def r2[P](implicit ev : P =:= B) = new Conforms[P , B] {}
  implicit def r3[P <: C] = new Conforms[P , C] {}
}


import TestableTypeClass._
import ImplicitContainer._

val b = new B
//b.test[A]    // - did not compile
b.test[B]      // - Ok
//b.test[C]    // - did not compile


回答2:

I think what you are trying to achieve is something like this:

//this is of course not a correct Scala code
def test[T <: upperBoundOf[Self]] 

But this doesn't make sense. Why? Because you can very easily circumvent such constraint, effectively rendering it pointless:

val h = new B
h.test[A] //nope, A is not a subtype of B

but...

val h: A = new B
h.test[A] //same thing, but this time it's apparently OK

Not only the constraint provides zero additional type safety, I think it also leads to breakage one of the most fundamental rules of OOP - the Liskov Substitution Principle. The snippet above compiles when h is of type A but does not compile when h is of type B even though it's a subtype of A, so everything should be fine according to the LSP.

So, essentially if you just leave your types like this:

class A extends Testable
class B extends A
trait Testable {
  def test[T <: A]
}

you have exactly the same level of type safety.