Search code examples
scalashapeless

Extensible record types


I'm attempting a simple exercise with shapeless' extensible records.

It's a typeclass called Projection, which should be able to somewhat combine the functionality of Updater and Remover:

import shapeless._
import shapeless.tag._
import shapeless.record._
import shapeless.labelled._
import shapeless.ops.record._
import shapeless.syntax._
// Probably way too many imports

trait Projection[A <: HList, K, V] {
    type B <: HList

    def to(a: A, v: V): B
    def from(b: B): A
}

object Projection {
    type Aux[A <: HList, K, V, B0 <: HList] = Projection[A, K, V] { type B = B0 }

    type Key[K] = Symbol with Tagged[K]
    type F[K, V] = V with FieldType[Key[K], V]

    implicit def mkProjection[A <: HList, K, V, B0 <: HList](implicit
        keyWitness: Witness.Aux[K],
        updater: Updater.Aux[A, F[K, V], B0],
        remover: Remover.Aux[B0, K, (V, A)]
    ): Projection.Aux[A, K, V, B0] = new Projection[A, K, V] {
        type B = B0

        def from(b: B0): A = b - keyWitness
        def to(a: A, v: V): B0 = a + field[Key[K]](v)
    }
}

My rather simple test

import Projection._

val thirdFieldWitness = Witness("thirdField")
val projector = implicitly[Projection[HNil, thirdFieldWitness.T, Boolean]]

unfortunately fails with the error

could not find implicit value for parameter e: Projection[shapeless.HNil,ProjectionSpec.this.thirdFieldWitness.T,Boolean]
[error]         val projector = implicitly[Projection[HNil, thirdFieldWitness.T, Boolean]]

-Xlog-implicits shows the reason for it:

ProjectionSpec.scala:18:35: record.this.Remover.mkRemover is not a valid implicit value for shapeless.ops.record.Remover.Aux[Boolean with shapeless.labelled.FieldType[Projection.Key[ProjectionSpec.this.thirdFieldWitness.T],Boolean] :: shapeless.HNil,ProjectionSpec.this.thirdFieldWitness.T,(Boolean, shapeless.HNil)] because:
[info] hasMatchingSymbol reported error: No field String("thirdField") in record type Boolean with shapeless.labelled.FieldType[Projection.Key[ProjectionSpec.this.thirdFieldWitness.T],Boolean] :: shapeless.HNil
[info]         val projector = implicitly[Projection[HNil, thirdFieldWitness.T, Boolean]]

Please help me understand this message and show me how to fix it.

Is there possibly an easier way to do this kind of extension and shortening of labelled generics?


Solution

  • Except -Xlog-implicits, one more standard way of debugging implicits is to resolve them manually and look at compile error.

    Try

    object Projection {
      type Aux[A <: HList, K, V, B0 <: HList] = Projection[A, K, V] { type B = B0 }
    
      type Key[K] = Symbol with Tagged[K]
      type F[K, V] = FieldType[Key[K], V]
    
      implicit def mkProjection[A <: HList, K, V, B0 <: HList](implicit
        keyWitness: Witness.Aux[Key[K]],
        updater: Updater.Aux[A, F[K, V], B0],
        remover: Remover.Aux[B0, Key[K], (V, A)]
      ): Projection.Aux[A, K, V, B0] = new Projection[A, K, V] {
        type B = B0
    
        def from(b: B0): A = b - keyWitness
        def to(a: A, v: V): B0 = a + field[Key[K]](v)
      }
    }
    

    Then

    implicitly[Projection.Aux[HNil, "thirdField", Boolean, Record.`'thirdField -> Boolean`.T]]
    

    compiles.

    But although implicitly[thirdFieldWitness.T =:= "thirdField"]

    implicitly[Projection.Aux[HNil, thirdFieldWitness.T, Boolean, Record.`'thirdField -> Boolean`.T]]
    

    still doesn't compile. But manually resolved

    implicitly[Projection.Aux[HNil,
      thirdFieldWitness.T,
      Boolean,
      Record.`'thirdField -> Boolean`.T
    ]](Projection.mkProjection(
      implicitly[Witness.Aux[Witness.`'thirdField`.T]],
      implicitly[Updater.Aux[HNil, FieldType[Witness.`'thirdField`.T, Boolean], Record.`'thirdField -> Boolean`.T]],
      implicitly[Remover.Aux[Record.`'thirdField -> Boolean`.T, Witness.`'thirdField`.T, (Boolean, HNil)]]
    ))
    

    compiles. The thing seems to be that implicitly[Witness.Aux[Key["thirdField"]]] compiles but implicitly[Witness.Aux[Key[thirdFieldWitness.T]]] doesn't ("Symbol with Tagged[thirdFieldWitness.T] is not a singleton type").

    You can fix compilation if you add

    implicit def extraWitness[S <: String](implicit 
      w: Witness.Aux[S]
    ): Witness.Aux[Symbol @@ S] = Witness.mkWitness(tag[S](Symbol(w.value)))
    

    I would use standard Symbol-based API

    object Projection {
      type Aux[A <: HList, K, V, B0 <: HList] = Projection[A, K, V] { type B = B0 }
    
      type F[K, V] = FieldType[K, V]
    
      implicit def mkProjection[A <: HList, K, V, B0 <: HList](implicit
        keyWitness: Witness.Aux[K],
        updater: Updater.Aux[A, F[K, V], B0],
        remover: Remover.Aux[B0, K, (V, A)]
      ): Projection.Aux[A, K, V, B0] = new Projection[A, K, V] {
        type B = B0
    
        def from(b: B0): A = b - keyWitness
        def to(a: A, v: V): B0 = a + field[K](v)
      }
    }
    
    implicitly[Projection.Aux[HNil, Witness.`'thirdField`.T, Boolean, Record.`'thirdField -> Boolean`.T]]
    
    val thirdFieldWitness = Witness('thirdField)
    implicitly[Projection.Aux[HNil, thirdFieldWitness.T, Boolean, Record.`'thirdField -> Boolean`.T]]