Home > other >  In Scala3, if generic type argument(s) is mapped to dependent type, how are covariant & contravarian
In Scala3, if generic type argument(s) is mapped to dependent type, how are covariant & contravarian

Time:01-10

In this article, it is explained that each generic type argument in Scala 3 code is perceived as a dependent type to be conform with the DOT logic:

https://dotty.epfl.ch/docs/internals/higher-kinded-v2.html

Namely:

The duality The core idea: A parameterized class such as

class Map[K, V]

is treated as equivalent to a type with type members:

class Map { type Map$K; type Map$V }

(the article may be obsolete but the design philosophy should still hold)

Consequentially, I would expect any contravariant & covariant modifiers for type arguments are also rewritten, so I did a quick experiment, the compiler should be able to convert the following code:

  object AsArg {

    trait P[ TT] {
      val vv: TT
    }

    trait P1 extends P[Product] {
      val vv: Product
    }
    trait P2 extends P1 with P[Tuple1[Int]] {
      val vv: Tuple1[Int]
    }
  }

into this:

  object AsDependentType {

    trait P {
      type TT

      val vv: TT
    }

    trait P1 extends P {
      type TT <: Product

      val vv: Product
    }

    trait P2 extends P1 with P {
      type TT <: Tuple1[Int]

      val vv: Tuple1[Int]
    }
  }

Ironically, after conversion, the compiler throw the following errors:

[Error] ...CovariantDependentType.scala:30:11: error overriding value vv in trait P of type P1.this.TT;
  value vv of type Product has incompatible type
[Error] ...CovariantDependentType.scala:36:11: error overriding value vv in trait P of type P2.this.TT;
  value vv of type Tuple1[Int] has incompatible type
two errors found

So what is the correct equivalent code after conversion?

CodePudding user response:

In/co/contra-variance is a property of a type constructor F[T].

F is co-variant if for all A <: B, F[A] <: F[B]. F is contra-variant if for all A <: B, F[B] <: F[A]. F is invariant if for all A <: B, F[A] and F[B] are <:-unrelated.

Type parameters and type members are different things both in Scala 2 and Scala 3.

For type parameters, variance can be set up at declaration site

trait F[ T] // co-variance
trait F[-T] // contra-variance
trait F[T] // invariance

or at call site

trait F[T]
type G[ T] = F[_ <: T] // co-variance
type G[-T] = F[_ >: T] // contra-variance

In Java there is no /-, so variance of type parameters has to be always set up with ? extends ..., ? super ... at call site.

For type members in Scala, there is no /- either, so variance has to be set up at call site

trait F { type T }
type G[ U] = F { type T <: U } // co-variance 
type G[-U] = F { type T >: U } // contra-variance

Type-parameter code

trait P[ TT] {
  val vv: TT
}

trait P1 extends P[Product] {
  val vv: Product
}

trait P2 extends P1 with P[Tuple1[Int]] {
  val vv: Tuple1[Int]
}

can be translated into type-member code in Scala 2 as

trait P {
  type TT
  val vv: TT1 forSome {type TT1 >: TT} // just val vv: _ >: TT is illegal here: unbound wildcard type
}

trait P1 extends P {
  type TT <: Product
  val vv: Product
}

trait P2 extends P1 with P {
  type TT <: Tuple1[Int]
  val vv: Tuple1[Int]
}

Since TT1 forSome {type TT1 >: TT} =:= Any, it's the same as

// (*)

trait P {
  type TT
  val vv: Any
}

trait P1 extends P {
  type TT <: Product
  val vv: Product
}

trait P2 extends P1 with P {
  type TT <: Tuple1[Int]
  val vv: Tuple1[Int]
}

Since existential types are recommended to be translated in Scala 3 as path-dependent types, this can be translated in Scala 3 as (*) or

trait P {
  type TT

  trait Inner {
    type TT1 >: TT
    val vv: TT1
  }

  val i: Inner
}

trait P1 extends P {
  type TT <: Product
}

trait P2 extends P1 with P {
  type TT <: Tuple1[Int]
}

CodePudding user response:

Prof @DmytroMitin's answer is mostly likely close to the actual compiled JVM bytecode in the original proposition. The constraint of which, if relies on pure DOT logic, may not be as tight as the original version. This shouldn't be a problem as additional verification steps (that doesn't belong to DOT logic) by the compiler can be superimposed to ensure the same level of safety.

But I'll post my answer that totally rely on DOT logic, which involves generating a companion Generic object for every combination of type arguments on the fly:

  
  object AsDependentType {

    trait Gen {

      type Upper

      type _CoV = { type T <: Upper }
      trait CoV extends P {
        type T <: Upper
        val vv: Upper
      }
    }

    trait P

    object GenY extends Gen {

      type Upper = Product
      trait P1 extends P with CoV {

        val vv: Product
      }
    }

    object GenZ extends Gen {

      type Upper = Tuple1[Int]
      trait P2 extends GenY.P1 with P with CoV {

        val vv: Tuple1[Int]
      }
    }

    implicitly[P with GenZ._CoV <:< P with GenY._CoV]
  }

The Gen interface can be a single trait for all higher kind with 1 argument. The ad-hoc implementation of CoV & ContraV are also created when F[A] <:< F[B] requires proving, e.g. the old:

P[Tuple1[Int]] <:< P[Product]

now becomes:

P with GenZ.CoV <:< P with GenY.CoV

No extra logic apart from DOT is required, at this moment, a compiler bug prevents it from compiling successfully (vv: Upper degrades to Product instead of GenZ.Upper), but if this is fixed, it should have no problem replacing the covariant decorator

  • Related