Search code examples
scalageneric-programmingshapeless

Generic type transformations with Shapeless


I currently started to experiment with Shapeless. My first try was the following code example. The Shapeless version is 2.3.0 and Scala version 2.11.7:

import org.scalatest._
import shapeless._

sealed trait Dog {
  def favoriteFood: String
}

sealed trait Cat{
  def isCute: Boolean
}

sealed trait Green

sealed trait Blue[G <: Green]{
  def makeGreen(): G = {
    val blueGen = LabelledGeneric[this.type]
    val greenGen = LabelledGeneric[G]
    val blue = blueGen.to(this)
    val green = greenGen.from(blue)
    green
  }
}

case class BlueDog(override val favoriteFood: String) extends Dog with Blue[GreenDog]
case class GreenDog(override val favoriteFood: String) extends Dog with Green

case class GreenCat(override val isCute: Boolean) extends Cat with Green
case class BlueCat(override val isCute: Boolean) extends Cat with Blue[GreenCat]

class ShapelessExperimentsTest extends FlatSpec with Matchers {

  "Make green" should "work" in {
    val blueDog = new BlueDog("Bones")
    val greenDog: GreenDog = blueDog.makeGreen
    assert(greenDog.favoriteFood == "Bones")

    val blueCat = new BlueCat(true)
    val greenCat: GreenCat = blueCat.makeGreen
    assert(greenCat.isCute)
  }
}

This code doesn't compile though because I didn't provide values for the implicit parameter lgen for the LabelledGenerics. The compile error therefore is

...ShapelessExperimentsTest.scala:16: could not find implicit value for parameter lgen: shapeless.LabelledGeneric[Blue.this.type]

and

...ShapelessExperimentsTest.scala:17: could not find implicit value for parameter lgen: shapeless.LabelledGeneric[G]

My problem is that I couldn't find out the correct way to provide those implicits to make the example work. Can anyone help me with this?


Solution

  • Sadly, in Scala, every "bit of generic -ness" has to be fixed at invocation site, this basically means that something like:

      val blueGen = LabelledGeneric[this.type]
      val greenGen = LabelledGeneric[G]
    

    In the body of a function doesn't compile because the compiler can't nail down "this.type" and "G". Luckily, implicits are in the language exactly to solve this problem:

    def makeGreen[T](implicit blueGen: LabelledGeneric.Aux[this.type, T], greenGen: LabelledGeneric.Aux[G, T]):
    

    (Aux is just a pattern to make computations on types, I highly suggest this article if you are not familiar with it)

    Again though, this code doesn't compile probably because the compiler can't infere that this.type is actually a case class and can't find an implicit LabelledGeneric instance.

    Instead, you can refactor your code to something like this:

    import org.scalatest._
    import shapeless._
    
    sealed trait Animal
    
    sealed trait Dog extends Animal {
      def favoriteFood: String
    }
    
    sealed trait Cat extends Animal {
      def isCute: Boolean
    }
    
    sealed trait Color
    sealed trait Green extends Color
    sealed trait Blue extends Color
    
    trait GreenColorable[A <: Animal, G <: Green] {
      def makeGreen[T](animal: A)(implicit animalGen: LabelledGeneric.Aux[A, T], greenGen: LabelledGeneric.Aux[G, T]): G = {
        val blue = animalGen.to(animal)
        val green = greenGen.from(blue)
    
        green
      }
    }
    
    object Colorables {
      def GreenColorable[A <: Animal, G <: Green] = new GreenColorable[A, G] {}
    }
    
    case class BlueDog(override val favoriteFood: String) extends Dog with Blue
    case class GreenDog(override val favoriteFood: String) extends Dog with Green
    
    case class GreenCat(override val isCute: Boolean) extends Cat with Green
    case class BlueCat(override val isCute: Boolean) extends Cat with Blue
    
    class ShapelessExperimentsTest extends FlatSpec with Matchers {
    
      "Make green" should "work" in {
        val blueDog = new BlueDog("Bones")
        val greenDogColorable= Colorables.GreenColorable[BlueDog, GreenDog]
        val greenDog = greenDogColorable.makeGreen(blueDog)
        assert(greenDog.favoriteFood == "Bones")
    
        val blueCat = new BlueCat(true)
        val greenCatColorable = Colorables.GreenColorable[BlueCat, GreenCat]
        val greenCat: GreenCat = greenCatColorable.makeGreen(blueCat)
        assert(greenCat.isCute)
      }
    }
    

    Here, the actual conversion is moved into a separate typeclass that takes the actual case class input and output types as parameters.