Shapeless: map from coproduct to different coprodu

2019-04-21 03:46发布

问题:

In the following, I'm trying to make a polymorphic function to convert a RawFeatureValue into a RefinedFeatureValue.

import shapeless._

object test {
  type RawFeatureValue = Int :+: Double :+: String :+: CNil
  type RefinedFeatureValue = Int :+: Double :+: CNil

  private object convert extends Poly1 {
    implicit def caseInt = at[Int](i => i)
    implicit def caseDouble = at[Double](d => d)
    implicit def caseString = at[String](s => s.hashCode)
  }

  val a = Coproduct[RawFeatureValue](12)
  val b: RefinedFeatureValue = a map convert
}

However, the resulting type is Int :+: Double :+: Int :+: CNil which is not compatible with RefinedFeatureValue.

[error]  found   : shapeless.:+:[Int,shapeless.:+:[Double,shapeless.:+:[Int,shapeless.CNil]]]
[error]  required: test.RefinedFeatureValue
[error]     (which expands to)  shapeless.:+:[Int,shapeless.:+:[Double,shapeless.CNil]]
[error]   val b: RefinedFeatureValue = a map convert
[error]                                  ^

How do I tell shapeless that the two Ints should be treated as one?

回答1:

The most straightforward way to do this I can think of would be to map each element into your target coproduct and then unify the result:

import shapeless._

type RawFeatureValue = Int :+: Double :+: String :+: CNil
type RefinedFeatureValue = Int :+: Double :+: CNil

object convert extends Poly1 {
  implicit val caseInt = at[Int](Coproduct[RefinedFeatureValue](_))
  implicit val caseDouble = at[Double](Coproduct[RefinedFeatureValue](_))
  implicit val caseString = at[String](s =>
    Coproduct[RefinedFeatureValue](s.hashCode))
}

This works as expected:

scala> val a = Coproduct[RawFeatureValue](12)
a: RawFeatureValue = 12

scala> val b: RefinedFeatureValue = a.map(convert).unify
b: RefinedFeatureValue = 12

scala> val c = Coproduct[RawFeatureValue]("foo")
c: RawFeatureValue = foo

scala> val d: RefinedFeatureValue = c.map(convert).unify
d: RefinedFeatureValue = 101574

This solution isn't bad, but it does seem like it might be useful enough to be a single operation.



回答2:

Alternatively, the methods on Coproducts allow to do it without a Poly (if your real use case allows it) - and less boilerplate. Let's define

def refine(v: RawFeatureValue): RefinedFeatureValue =
  v.removeElem[String]
    .left.map(s => Coproduct[RefinedFeatureValue](s.hashCode))
    .merge

Then you can do

scala> val a = Coproduct[RawFeatureValue](12)
a: RawFeatureValue = 12

scala> refine(a)
res1: RefinedFeatureValue = 12

scala> val c = Coproduct[RawFeatureValue]("foo")
c: RawFeatureValue = foo

scala> refine(c)
res2: RefinedFeatureValue = 101574

This consists in:

  • splitting RawFeatureValue into a String on the one hand, and the remaining elements on the other (which make a RefinedFeatureValue), with removeElem, returning a Either[String, RefinedFeatureValue],
  • mapping on the left of the result, to transform it and pack it into a RefinedFeatureValue,
  • and merge the resulting Either[RefinedFeatureValue, RefinedFeatureValue] into a single RefinedFeatureValue.


回答3:

Alternatively, if you aren't set on a polymorphic function you can use the convert type class in this library: https://github.com/xdotai/typeless/

Here is a convert type class that transforms one Coproduct to an Option of another Coproduct: https://github.com/xdotai/typeless/blob/master/src/main/scala/coproduct/convert.scala

Add to dependencies:

libraryDependencies += "ai.x" %% "typeless" % "0.2.5"

Code:

scala> import ai.x.typeless.coproduct.Convert.Ops
import ai.x.typeless.coproduct.Convert.Ops

scala> import shapeless._
import shapeless._

scala> type A = String :+: Double :+: CNil
defined type alias A

scala> type B = Double :+: String :+: List[Int] :+: CNil
defined type alias B

scala> Coproduct[A]("test").convert[B]
res0: Option[B] = Some(Inr(Inl(test)))