scalagenericstypestype-systemstype-bounds

How to avoid duplication of type bound in Scala


I have a class with a constrained type parameter.

I've tried with identity but the return type is not precise.

And in the method identityTP, I need to specify again the constraint

How to avoid to duplicate this constraint with methods that work on this type ?

Here is an example :

  sealed trait Location
  case object Single extends Location
  case object Multi extends Location

  final case class Log[L <: Location](l: L)

  def identity(log: Log[_]): Log[_] = log
  def identityTP[L<: Location](log: Log[L]): Log[L] = log

Solution

  • Actually this is not a duplication. L in

    final case class Log[L <: Location](l: L)
    

    and L in

    def identityTP[L <: Location](log: Log[L]): Log[L] = log
    

    are two completely different type parameters. This could be more clear if you used different identifiers

    final case class Log[L <: Location](l: L)
    def identityTP[L1 <: Location](log: Log[L1]): Log[L1] = log
    

    An upper bound for one type parameter doesn't duplicate an upper bound for other type parameter.

    Also this is not a duplication because actually upper bounds can be different

    sealed trait SubLocation extends Location
    final case class Log[L <: Location](l: L)
    def identityTP[L1 <: SubLocation](log: Log[L1]): Log[L1] = log
    

    If you don't want to create the second type parameter you can make identityTP nested into Log (making it Log's method)

    final case class Log[L <: Location](l: L) {
      def identityTP: Log[L] = this
    }
    

    Sometimes it can help if you make L a type member rather than type parameter

    trait Log {
      type L <: Location
      val l: L
    }
    object Log {
    //  def apply[_L <: Location](_l: _L): Log { type L = _L} = new Log {
    //    override type L = _L
    //    override val l: L = _l
    //  }
    
      def apply[_L <: Location](_l: _L): Log = new Log {
        override type L = _L
        override val l: L = _l
      }
    }
    
    //  def identityTP(log: Log): Log = log
    def identityTP(log: Log): Log { type L = log.L } = log
    

    Notice that although we have to repeat upper bound in apply but we don't have in identityTP.

    Normally it's not a big deal to repeat upper bound when necessary

    class MyClass[A <: A1]
    def foo[A <: A1](mc: MyClass[A]) = ???
    def bar[A <: A1](mc: MyClass[A]) = ???
    

    When this becomes cumbersome

    class MyClass[A <: A1, B <: B1, C <: C1]
    def foo[A <: A1, B <: B1, C <: C1](mc: MyClass[A, B, C]) = ???
    def bar[A <: A1, B <: B1, C <: C1](mc: MyClass[A, B, C]) = ???
    

    you should redesign your abstractions. For example

    trait Tuple {
      type A <: A1
      type B <: B1
      type C <: C1
    }
    class MyClass[T <: Tuple]
    def foo[T <: Tuple](mc: MyClass[T]) = {
      //T#A, T#B, T#C instead of A, B, C
      ???
    }
    

    or

    class MyClass[T <: Tuple](val t: T)
    //class MyClass(val t: Tuple)
    
    def foo[T <: Tuple](mc: MyClass[T]) = {
    //def foo(mc: MyClass) = {
      import mc.t
      //t.A, t.B, t.C instead of A, B, C
      ???
    }
    

    Also sometimes you can play with replacing type bounds with type constraints

    final case class Log[L](l: L)(implicit ev: L <:< Location)
    def identityTP[L](log: Log[L])(implicit ev: L <:< Location): Log[L] = log
    

    Although this doesn't remove repetitions but there are ways to fight against repetitions among implicit parameters as well (type classes). See How to wrap a method having implicits with another method in Scala?