Steve Pletcher
Steve Pletcher

Reputation: 49

Expanding Option[_] over a Product in Shapeless

I'm learning my way through Shapeless, and I have a specific behavior I'm looking to implement to allow our code to expand out optional case classes identically whether they're present or not. Basically:

Given an Option[N] where N <: Product, I'd like to produce an HList with the same types produced by Generic[N].to, but with each element wrapped in an Option if it isn't already. e.g.:

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

optionalize[Foo](Some(Foo("abc", Some(123L), None)))
// => Some("abc") :: Some(123L) :: None :: HNil

optionalize[Foo](None)
// => None :: None :: None :: HNil

// where optionalize.Out = Option[String] :: Option[Long] :: Option[String] :: HNil

My ultimate goal is the ability to flatten out nested case classes, so I'd like to introduce this as a rule such that Shapeless can do this automatically via type inference. The biggest obstacle in my mind is understanding how to write the None case. Currently, my code looks something like this:

trait LowPriEnsureOptional extends Poly1 {
  implicit def somethingCase[In]: Case.Aux[In, Option[In]] = at(thing => Some(thing))
  implicit val hnilCase: Case.Aux[HNil, HNil] = at(identity)
}

object EnsureOptional extends LowPriEnsureOptional {
  implicit def optionCase[In <: Option[_]]: Case.Aux[In, In] = at(identity)
}

object OptionizeHlist {

  def optionizeCaseClass[
    CC <: Product,
    R <: HList
  ](occ: Option[CC])(
    implicit gen: Generic[CC] { type Repr = R },
    optionalize: Mapper[EnsureOptional.type, R]
  ): optionalize.Out =
    occ match {
      case Some(cc) => optionalize.apply(gen.to(cc))
      case None     => ???
    }
}

To write out the None case, I need some way to, given a case class, get its generic repr, run it through OptionizeHlist, and generate an instance of it consisting entirely of Nones, but I have no idea where to start on this.

Upvotes: 1

Views: 108

Answers (1)

HTNW
HTNW

Reputation: 29193

Notice that, if you are passed a None, type erasure makes it so that you cannot know how many Nones to put into the output list. Therefore, you need another implicit argument to survive that information to runtime.

final class AllNoneable[H <: HList] private (val allNone: H) extends AnyVal
object AllNoneable {
    implicit val allNoneableHNil = new AllNoneable[HNil](HNil)
    implicit def allNoneableCons[H >: None.type, T <: HList](implicit t: AllNoneable[T])
    = new AllNoneable[H :: T](None :: t.allNone)
}

Also, hnilCase does nothing.

trait LowPriEnsureOptional extends Poly1 {
  implicit def somethingCase[In]: Case.Aux[In, Option[In]] = at(Some(_))
}
object EnsureOptional extends LowPriEnsureOptional {
  implicit def optionCase[In <: Option[_]]: Case.Aux[In, In] = at(identity)
}
def optionalizeCase
  [C, Rep <: HList, Opt <: HList](c: Option[C])
  (implicit
    gen: Generic.Aux[C, Rep], opt: Mapper.Aux[EnsureOptional.type, Rep, Opt],
    nones: AllNoneable[Opt]): Opt
= c match {
    case Some(c) => opt.apply(gen.to(c))
    case None => nones.allNone
}

Scastie

Upvotes: 1

Related Questions