Search code examples
scalashapeless

shapeless convert case class to HList and skip all option fields


I have the next class:

case class Foo(a: Option[Int], b: Option[String], c: Option[Double])

as you can see, all fields is optional, i want convert this class into HList or Tuple, like

val f1 = Foo(Some(1) , None, Some(3D))
val f2 = Foo(None, "foo")

val result1 = f1.to[Int::Double::HNil] // => 1::3D
val result2 = f2.to[String::HNil] // "foo"

is it possible, without reflection?


Solution

  • It might be possible to do this with existing type classes in Shapeless (something like NatTRel and RemoveAll), but I'm not 100% sure of that, and this is a case where I'd just write my own type class:

    import shapeless._
    
    trait OptionalPieces[L <: HList, S <: HList] {
      def apply(l: L): Option[S]
    }
    
    object OptionalPieces extends LowPriorityOptionalPieces {
      implicit val hnilOptionalPieces: OptionalPieces[HNil, HNil] =
        new OptionalPieces[HNil, HNil] {
          def apply(l: HNil): Option[HNil] = Some(HNil)
        }
    
      implicit def hconsOptionalPiecesMatch[H, T <: HList, S <: HList](implicit
        opt: OptionalPieces[T, S]
      ): OptionalPieces[Option[H] :: T, H :: S] =
        new OptionalPieces[Option[H] :: T, H :: S] {
          def apply(l: Option[H] :: T): Option[H :: S] = for {
            h <- l.head
            t <- opt(l.tail)
          } yield h :: t
        }
    }
    
    sealed class LowPriorityOptionalPieces {
      implicit def hconsOptionalPiecesNoMatch[H, T <: HList, S <: HList](implicit
        opt: OptionalPieces[T, S]
      ): OptionalPieces[Option[H] :: T, S] =
        new OptionalPieces[Option[H] :: T, S] {
          def apply(l: Option[H] :: T): Option[S] = opt(l.tail)
        }
    }
    

    This witnesses that L contains at least all of the elements of S wrapped in Option, in order, and gives you a way to unwrap them at runtime (safely).

    We can then define a syntax helper class like this:

    implicit class OptionalPiecesSyntax[A, R <: HList](a: A)(implicit
      gen: Generic.Aux[A, R]
    ) {
      def to[S <: HList](implicit op: OptionalPieces[gen.Repr, S]): Option[S] =
        op(gen.to(a))
    }
    

    And then:

    scala> val f1 = Foo(Some(1) , None, Some(3D))
    f1: Foo = Foo(Some(1),None,Some(3.0))
    
    scala> val f2 = Foo(None, Some("foo"), None)
    f2: Foo = Foo(None,Some(foo),None)
    
    scala> val result1 = f1.to[Int :: Double :: HNil]
    result1: Option[shapeless.::[Int,shapeless.::[Double,shapeless.HNil]]] = Some(1 :: 3.0 :: HNil)
    
    scala> val result2 = f2.to[String :: HNil]
    result2: Option[shapeless.::[String,shapeless.HNil]] = Some(foo :: HNil)
    

    If you really wanted exceptions, you could just call .get in the syntax class, but that seems like a bad idea.