Search code examples
scalashapeless

How do I best construct a shapeless record with a default value?


Say I have a shapeless record:

trait T
case object Ti extends T
case object Ta extends T
case object To extends T

type R = Record.`Ta -> Option[String], Ti -> Option[Int]`.T
val r: R = (Ta ->> Option("plif")) :: (Ti ->> Option(4)) :: HNil

I'd like to write a function get such that:

get(Ta) == Some("plif")
get(Ti) == Some(4)
get(To) == None

What would be the best way to achieve this?


Solution

  • A simple solution is to provide your own Selector instance for a default case:

    class DefaultSelector[R <: HList, K] extends Selector[R, K] {
      type Out = Option[Nothing]
      def apply(l: R): Out = None
    }
    
    def get[K, V](k: K)(
      implicit sel: Selector[R, K] = new DefaultSelector[R, K]
    ): sel.Out = sel(r)
    

    But with that code Scala's compiler may have difficulties providing TypeTags for the result of the default case.


    So to fix that you can also write a new typeclass DefaultSelector, which will default to None: Option[Nothing], if no Selector is found:

    import shapeless._
    import shapeless.ops.record._
    
    trait DefaultSelector[R <: HList, K] {
      type Out
      def apply(r: R): Out
    }
    
    sealed trait LowPriorityDefaultSelector {
      type Aux[R <: HList, K, V] = DefaultSelector[R, K] { type Out = V }
    
      case class Impl[R <: HList, K, V](get: R => V) extends DefaultSelector[R, K] {
        type Out = V
        def apply(r: R): Out = get(r)
      }
    
      implicit def default[R <: HList, K, V](
        implicit ev: Option[Nothing] =:= V  // tricking Scala's implicit resolution
      ): Aux[R, K, V] =
        Impl[R, K, V](Function.const(None))
    }
    
    object DefaultSelector extends LowPriorityDefaultSelector {
      implicit def existing[R <: HList, K, V](
        implicit sel: Selector.Aux[R, K, V]
      ): Aux[R, K, V] =
        Impl[R, K, V](sel.apply)
    }
    

    Then the get function becomes:

    def get[K, V](k: K)(implicit sel: DefaultSelector[R, K]): sel.Out = sel(r)
    

    And the result (for both solutions) is:

    scala> get(Ti)
    res0: Option[Int] = Some(4)
    
    scala> get(Ta)
    res1: Option[String] = Some(plif)
    
    scala> get(To)
    res2: Option[Nothing] = None