How to “extract” type parameter to instantiate ano

2020-02-29 07:05发布

问题:

The following Scala code works:

object ReducerTestMain extends App {

  type MapOutput = KeyVal[String, Int]

  def mapFun(s:String): MapOutput = KeyVal(s, 1)

  val red = new ReducerComponent[String, Int]((a: Int, b: Int) => a + b)

  val data = List[String]("a", "b", "c", "b", "c", "b")

  data foreach {s => red(mapFun(s))}
  println(red.mem)
  // OUTPUT: Map(a -> 1, b -> 3, c -> 2)
}

class ReducerComponent[K, V](f: (V, V) => V) {
  var mem = Map[K, V]()

  def apply(kv: KeyVal[K, V]) = {
    val KeyVal(k, v) = kv
    mem += (k -> (if (mem contains k) f(mem(k), v) else v))
  }
}

case class KeyVal[K, V](key: K, value:V)

My problem is I would like to instantiate ReducerComponent like this:

val red = new ReducerComponent[MapOutput, Int]((a: Int, b: Int) => a + b)

or even better:

val red = new ReducerComponent[MapOutput](_ + _)

That means a lot of things:

  1. I would like to type-check that MapOutput is of the type KeyVal[K, C],
  2. I want to type-check that C is the same type used in f,
  3. I also need to "extract" K in order to instantiate mem, and type-check parameters from apply.

Is it a lot to ask? :) I wanted to write something like

class ReducerComponent[KeyVal[K,V]](f: (V, V) => V) {...}

By the time I will instantiate ReducerComponent all I have is f and MapOutput, so inferring V is OK. But then I only have KeyVal[K,V] as a type parameter from a class, which can be different from KeyVal[_,_].

I know what I'm asking is probably crazy if you understand how type inference works, but I don't! And I don't even know what would be a good way to proceed --- apart from making explicit type declarations all the way up in my high-level code. Should I just change all the architecture?

回答1:

Just write a simple factory:

case class RC[M <: KeyVal[_, _]](){
   def apply[K,V](f: (V,V) => V)(implicit ev: KeyVal[K,V] =:= M) = new ReducerComponent[K,V](f)
}

def plus(x: Double, y: Double) = x + y

scala> RC[KeyVal[Int, Double]].apply(plus)
res12: ReducerComponent[Int,Double] = ReducerComponent@7229d116

scala> RC[KeyVal[Int, Double]]()(plus)
res16: ReducerComponent[Int,Double] = ReducerComponent@389f65fe

As you can see, ReducerComponent has appropriate type. Implicit evidence is used here to catch K and V from your M <: KeyVal[_, _].

P.S. The version above requires to specify parameter types explicitly for your f, like (_: Double) + (_: Double). If you want to avoid this:

case class RC[M <: KeyVal[_, _]](){
   def factory[K,V](implicit ev: KeyVal[K,V] =:= M) = new {
      def apply(f: (V,V) => V) = new ReducerComponent[K,V](f)
   }
}

scala> RC[KeyVal[Int, Double]].factory.apply(_ + _)
res5: ReducerComponent[Int,Double] = ReducerComponent@3dc04400 


scala> val f = RC[KeyVal[Int, Double]].factory
f: AnyRef{def apply(f: (Double, Double) => Double): ReducerComponent[Int,Double]} = RC$$anon$1@19388ff6

scala> f(_ + _)
res13: ReducerComponent[Int,Double] = ReducerComponent@24d8ae83

Update. If you want to generelize keyval - use type function:

type KV[K,V] = KeyVal[K,V] //may be anything, may implement `type KV[K,V]` from some supertrait

case class RC[M <: KV[_, _]](){   
  def factory[K,V](implicit ev: KV[K,V] =:= M) = new {
    def apply(f: (V,V) => V) = new ReducerComponent[K,V](f)
  }
}

But keep in mind that apply from your question still takes KeyVal[K,V].

You can also pass KV into some class:

class Builder[KV[_,_]] {
  case class RC[M <: KV[_, _]](){   
    def factory[K,V](implicit ev: KV[K,V] =:= M) = new {
      def apply(f: (V,V) => V) = new ReducerComponent[K,V](f)
    }
  }
}

scala> val b = new Builder[KeyVal]
scala> val f = b.RC[KeyVal[Int, Double]].factory
scala> f(_ + _)
res2: ReducerComponent[Int,Double] = ReducerComponent@54d9c993


回答2:

You will need path-dependent types for this. I recommend the following:

First, write a trait that has your relevant types as members, so you can access them within definitions:

trait KeyValAux {
  type K
  type V
  type KV = KeyVal[K, V]
}

Now you can create a factory for ReducerComponent:

object ReducerComponent {
  def apply[T <: KeyValAux](f: (T#V, T#V) => T#V) =
    new ReducerComponent[T#K, T#V](f)
}

Note that here, we can simply access the members of the type. We can't do this for type parameters.

Now, define your MapOutput in terms of KeyValAux (maybe another name is more appropriate for your use case):

type MapOutput = KeyValAux { type K = String; type V = Int }

def mapFun(s:String): MapOutput#KV = KeyVal(s, 1)

val red = ReducerComponent[MapOutput](_ + _)

UPDATE

As @dk14 mentions in the comments, if you still want the type-parameter syntax, you could do the following:

trait OutputSpec[KK, VV] extends KeyValAux {
  type K = KK
  type V = VV
}

You can then write:

type MapOutput = OutputSpec[String, Int]

Alternatively, you can write OutputSpec as a type function:

type OutputSpec[KK, VV] = KeyValAux { type K = KK; type V = VV }

This will not generate an additional unused class.