Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic derivation of type class instance for ADT

Suppose I've got an ADT and type class Foo like this:

sealed trait A
case class A1() extends A
case class A2() extends A
case class A3() extends A

trait Foo[X] { def foo(x: X): String; }
object Foo {
  implicit val a1foo = new Foo[A1] { def foo(a1: A1) = "A1" }
  implicit val a2foo = new Foo[A2] { def foo(a2: A2) = "A2" }
  implicit val a3foo = new Foo[A3] { def foo(a3: A3) = "A3" }
}  

Now I can write Foo[A] like that:

implicit val afoo = new Foo[A] {
  def foo(a: A) = a match {
    case a1 : A1 => a1foo.foo(a1)
    case a2 : A2 => a2foo.foo(a2)
    case a3 : A3 => a3foo.foo(a3)
  }
}

Unfortunately this code is too boilerplaty. Is it possible to get rid of all that boilerplate and derive Foo[A] automatically (perhaps with shapeless) ?

like image 404
Michael Avatar asked Nov 09 '22 12:11

Michael


1 Answers

This afoo implicit is not only useless in the presence of the other three, but even bad, because it will fail with a MatchError on the new A {} value.

I don't see why you would need such instance of Foo[A] when you have implicits that cover all possible (valid) values of type A and afoo doesn't add anything.

I can imagine that if you have a function

def foo(a: A)(implicit f: Foo[A]): String = f.foo(a)

Then, of course, neither of a1foo, a2foo or a3foo will fit. afoo will, so foo(A1()) will compile and work fine, but foo(new A {}) will also compile, and fail with a MatchError. By the way, if the call foo(new A {}) is present in code, it will compile with a warning about non-exhaustive match, but if it's not, it will compile silently.

So the solution to this is to modify foo, to take a more precise type:

def foo[X <: A](a: X)(implicit f: Foo[X]): String = f.foo(a)

Now foo(A1()) will compile and pick a1foo (same with A2 and A3), while foo(new A {}) just won't compile (because it shouldn't).


Update

If you still want to have an instance of Foo[A] as a default case, you can change your code like this:

object Foo extends Foo_1 {

  implicit val a1foo: Foo[A1] = ...
  implicit val a2foo: Foo[A2] = ...
  implicit val a3foo: Foo[A3] = ...
}

trait Foo_1 {
  // this implicit has a lower priority:
  implicit val afoo: Foo[A] = new Foo[A] { def foo(a: A) = "A" }
}

Now foo(new A {}) or foo(A2(): A) will compile and both return "A".

P.S. by the way, it's recommended to write explicit type of implicits.

like image 176
laughedelic Avatar answered Nov 14 '22 23:11

laughedelic