简体   繁体   中英

Shapeless: what the difference between these two approaches of instance derivation?

Can someone explain me what the difference between these two approaches for typeclass instance derivation (specifically for Option[A])?

1.

trait MyTrait[A] {...}

object MyTrait extends LowPriority {
 // instances for primitives
}

trait LowPriority extends LowestPriority {
 final implicit def generic[A, H <: HList](
    implicit gen: Generic.Aux[A, H],
    h: Lazy[MyTrait[H]]
  ): MyTrait[A] = ???

  final implicit val hnil: MyTrait[HNil] = ???

  final implicit def product[H, T <: HList](
    implicit h: Lazy[MyTrait[H]],
    t: Lazy[MyTrait[T]]
  ): MyTrait[H :: T] = ???
}

// special instances for Options
trait LowestPriority {
  implicit def genericOption[A, Repr <: HList](
    implicit gen: Generic.Aux[A, Repr],
    hEncoder: Lazy[MyTrait[Option[Repr]]]
  ): MyTrait[Option[A]] = ???

  implicit val hnilOption: MyTrait[Option[HNil]] = ???

  implicit def productOption1[H, T <: HList](
    implicit
    head: Lazy[MyTrait[Option[H]]],
    tail: Lazy[MyTrait[Option[T]]],
    notOption: H <:!< Option[Z] forSome { type Z }
  ): MyTrait[Option[H :: T]] = ???

  implicit def product2[H, T <: HList](
    implicit
    head: Lazy[MyTrait[Option[H]]],
    tail: Lazy[MyTrait[Option[T]]
  ): MyTrait[Option[Option[H] :: T]] = ???
}
trait MyTrait[A] {...}

object MyTrait extends LowPriority {
 // instances for primitives
}

trait LowPriority {
// deriving instances for options from existing non-option instances
 final implicit def forOption[A](implicit instance: MyTrait[A]): MyTrait[Option[A]] = ??? // <<<----

 final implicit def generic[A, H <: HList](
    implicit gen: Generic.Aux[A, H],
    h: Lazy[MyTrait[H]]
  ): MyTrait[A] = ???

  final implicit val hnil: MyTrait[HNil] = ???

  final implicit def product[H, T <: HList](
    implicit h: Lazy[MyTrait[H]],
    t: Lazy[MyTrait[T]]
  ): MyTrait[H :: T] = ???
}

I tried both and they worked correctly, but i'm not sure that they will produce the same results for all cases (maybe i've missed something).

Do we really need LowestPriority instances for this? Am i right if i would say that the first approach gives us just a little bit more flexibility?

Actually it's hard to say without right hand sides and actual implementations.

From information you provided it doesn't follow that the two type classes behave equivalently.

For example in the 1st approach you consider some special cases, so theoretically it's possible that you redefine some general behavior in special case differently.

By the way, Option[A] is a coproduct of Some[A] and None.type ( List[A] is a coproduct of scala.::[A] and Nil.type ) and sometimes it's easier to derive a type class for coproducts than for Option[A] (or List[A] ).

I assuming that by "worked correctly" you mean "compiled" or "worked for some simple use case".

Both of your examples deal with generic product types, but not with generic sum types, so there is no risk that eg Option[A] could get derived using Some[A]:+: None:+: CNil , which would enforce some ambiguity. So (as far as I can tell) you could write the second version like:

trait MyTrait[A] {...}

object MyTrait extends LowPriority {
 // instances for primitives

// deriving instances for options from existing non-option instances
 final implicit def forOption[A](implicit instance: MyTrait[A]): MyTrait[Option[A]] = ??? 
}

trait LowPriority {
// <<<----
 final implicit def hcons[A, H <: HList](
    implicit gen: Generic.Aux[A, H],
    h: Lazy[MyTrait[H]]
  ): MyTrait[A] = ???

  final implicit val hnil: MyTrait[HNil] = ???

  final implicit def product[H, T <: HList](
    implicit h: Lazy[MyTrait[H]],
    t: Lazy[MyTrait[T]]
  ): MyTrait[H :: T] = ???
}

and it would derive things correctly.

But how 1. and 2. differs?

In second version you can derive MyTrait[Option[A]] if you can derive for A , and you can derive for any A which is primitive/option/product - so Option[Option[String]] , Option[String] and Option[SomeCaseClass] should all work. It should also work if this SomeCaseClass contains fields which are Option s, or other case classes which are Option s, etc.

Version 1. is slightly different:

  • at first you are looking for primitives
  • then you try to derive for a product (so eg Option would not be handled here)
  • then you do something weird:
    • genericOption assumes that you created a Option[Repr] , and then I guess map it using Repr
    • in order to build that Repr you take Option[HNil] and prepend types inside Option using productOption , which would break if someone used Option as a field
    • so you "fix" that by prepending an Option in a special case product2

I guess, you tested that only against case classes, because the first version would not work for:

  • Option for primitives ( Option[String] , Option[Int] or whatever you defined as primitive)
  • nested options ( Option[Option[String]] )
  • options for custom defined types which are not case classes but have manually defined instances:
     class MyCustomType object MyCustomType { implicit val myTrait: MyTrait[MyCustomType] } implicitly[Option[MyCustomType]]

For that reason any solution with implicit def forOption[A](implicit instance: MyTrait[A]): MyTrait[Option[A]] is simpler and more bulletproof.

Depending on what you put directly into companion low-priority implicits might be or might not be needed:

  • if you defined coproducts then manual support for eg Option , List , Either could conflict with shapeless derived ones
  • if you manually implemented MyTrait implicit for some type in its companion object then it would have the same priority as implicits directly in MyTrait - so if it could be derived using shapeless you could have conflicts

For that reason it makes sense to put shapeless implicits in LowPriorityImplicits but primitives, and manual codecs for List, Option, Either, etc directly in companion. That is, unless you defined some eg Option[String] implicits directly in companion which could clash with " Option[A] with implicit for A ".

Since I don't know your exact use case I cannot tell for sure, but I would probably go with the seconds approach, or most likely with the one I implemented in the snippet above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM