sealed trait Character case class Player(hp: Int) extends Character case class Civilian(name: String, hp: Int) extends Character case class Monster(hp: Int, weakness: String) extends Character object HittableDemo extends App { import Hittable.ops._ val c1: Character = Player(1) assert(c1.hit == Player(0)) val c2 = Civilian("John", 3) assert(c2.hit == Civilian("John", 1)) assert((c2: Civilian).hit == Civilian("John", 1)) val c3: Character = Monster(2, "Fire") assert(c3.hit == Monster(1, "Fire")) assert(Civilian("John", 3).hit.name == "John") assert(Monster(2, "Fire").hit.weakness == "Fire") } trait Hittable[T] { def apply(t: T): T } object Hittable { object ops { implicit class HitOps[T: Hittable](t: T) { def hit: T = implicitly[Hittable[T]] apply t } } // 'Override' default behavior by providing more specific instance implicit val civilianInstance: Hittable[Civilian] = new Hittable[Civilian] { def apply(c: Civilian): Civilian = c.copy(hp = c.hp - 2) } // Default instances derivation def defaultHitFunc(hp: Int) = hp - 1 import shapeless._ import shapeless.ops.record._ private val wHp = Witness('hp) implicit def genProdHittable[T, Repr <: HList] (implicit prod: HasProductGeneric[T], gen: LabelledGeneric.Aux[T, Repr], modifier: Modifier.Aux[Repr, wHp.T, Int, Int, Repr] ): Hittable[T] = new Hittable[T] { def apply(t: T): T = gen.from(modifier(gen.to(t), defaultHitFunc)) } implicit def cnilHittable: Hittable[CNil] = new Hittable[CNil] { def apply(t: CNil): CNil = t } implicit def cconsHittable[H, T <: Coproduct, F, A] (implicit uh: Lazy[Hittable[H]], ut: Lazy[Hittable[T]] ): Hittable[H :+: T] = new Hittable[H :+: T] { def apply(t: H :+: T): H :+: T = t match { case Inl(h) => Inl(uh.value(h)) case Inr(t) => Inr(ut.value(t)) } } implicit def genCoprodHittable[T, Repr <: Coproduct] (implicit coprod: HasCoproductGeneric[T], gen: Generic.Aux[T, Repr], hittable: Lazy[Hittable[Repr]] ): Hittable[T] = new Hittable[T] { def apply(t: T): T = gen.from(hittable.value(gen.to(t))) } }