kaychaks
kaychaks

Reputation: 1775

Scala Typeclass with multiple parameters error

I'm getting a strange error related to types with this multi-parameter typeclass implementation

trait Feedtype
trait Atom extends Feedtype
trait Rss2 extends Feedtype
case object Atom extends Atom
case object Rss2 extends Rss2

trait Encoding
trait Xml extends Encoding
trait Json
case object Json extends Json
case object Xml extends Xml

trait Content
case class show[T <: Feedtype,E <: Encoding](str: String, tp: T, en: E) extends Content

trait TypeClass[C,D,E] {
  def show(c: C, d: D, e:E): String
}

trait Service{
  def print[T,C,D](t: T,c:C, d:D)(implicit s: TypeClass[T,C,D]): String
}

object Service extends Service{
  def print[T,C,D](t:T, c:C, d:D)(implicit s: TypeClass[T,C,D]): String =
    s.show(t,c,d)
}

implicit val t1 = new TypeClass[show[Atom,Xml], Atom, Xml] {
  def show(c: show[Atom,Xml], d:Atom, e:Xml) = c.str
}

implicit val t2 = new TypeClass[show[Rss2,Xml], Rss2, Xml] {
  def show(c: show[Rss2,Xml], d:Rss2, e:Xml) = "hi there " + c.str
}

val s1 = show("some show", Atom, new Xml {})

Service.print(s1, s1.tp, s1.en)

The error I'm getting is

type mismatch;
 found   : A$A10.this.typeclass[A$A10.this.show[A$A10.this.atom,A$A10.this.xml],A$A10.this.atom,A$A10.this.xml]
 required: A$A10.this.typeclass[A$A10.this.show[A$A10.this.atom.type,A$A10.this.xml.type],A$A10.this.atom.type,A$A10.this.xml.type]
service.print(s1, s1.tp, s1.en)(t1);}
                                ^

What am I missing here ?


Update

I figured out that issue is that atom and xml being case objects when they are being used as values to create s1. If I use new atom {} or new xml {} then the execution is happening fine. However, I wonder why are the case objects being considered a different type than the types that they represent ?

Upvotes: 2

Views: 463

Answers (1)

Michael Zajac
Michael Zajac

Reputation: 55569

As already pointed out the problem lies here:

val s1 = show("some show", Atom, new Xml {})
                //           ^
                // The most specific type of this is Atom.type

Service#print has its type parameters inferred by the objects passed to it. s1 is a show[Atom.type, Xml], therefore the compiler is looking for an implicit TypeClass[show[Atom.type, Xml], Atom.type, Xml] for Service#print. The compiler will not automatically try to upcast an Atom.type (the cast object) to an Atom for a couple reasons:

  1. The compiler does not know a show[Atom.type, Xml] is also a show[Atom, Xml] (show is declared as invariant, and so is TypeClass).

  2. There is an implicit TypeClass[show[Atom,Xml], Atom, Xml] available for the generic Atom, but not for the more specific type Atom.type.


Your example is very complicated, but doesn't need to be to reproduce the issue. Consider this:

trait TypeClass[A] {
    def show(a: A): String
}

trait Atom
case object Atom extends Atom

implicit val ar = new TypeClass[Atom] {
    def show(a: Atom): String = "Atom"
}

object Service {
    def print[A](a: A)(implicit tc: TypeClass[A]) = tc.show(a)
}

scala> Service.print(Atom)
<console>:18: error: could not find implicit value for parameter tc: TypeClass[Atom.type]
       Service.print(Atom)
                    ^

We get the same compilation error. This is because in the above structure, a TypeClass[Atom.type] is not the same as a TypeClass[Atom] as far as the compiler is concerned, but it doesn't have to be that way. We can make TypeClass contravariant over A, which means that a TypeClass[Atom] may be substituted in place of a TypeClass[Atom.type]. It makes sense to do so in this context because TypeClass in my example represents a printer-like class. If you know how to print the super-type, you can print a sub-type, but not necessarily the other way around.

trait TypeClass[-A] {
    def show(a: A): String
}

This will allow the implicit to resolve now. If you want to keep TypeClass invariant, we can up-cast the Atom ourselves:

scala> Service.print(Atom: Atom)
res4: String = Atom

How to apply this to your example?

The easy way would be to up-cast Atom:

val s1 = show("some show", Atom: Atom, new Xml {})

Or, show and TypeClass cannot be invariant. First, make show covariant over T so that a show[Atom.type, B] is also a show[Atom, B]:

case class show[+T <: Feedtype, E <: Encoding](str: String, tp: T, en: E) extends Content

Then, make TypeClass contravariant over C and D, so that a TypeClass[show[Atom,Xml], Atom, Xml] can also be considered a TypeClass[show[Atom.type, Xml], Atom.type,Xml], which will allow the implicit t1 to be picked up.

trait TypeClass[-C, -D, E] {
  def show(c: C, d: D, e:E): String
}

// cutting out putting it all together to save space

scala> val s1 = show("some show", Atom, new Xml {})
s1: show[Atom.type,Xml] = show(some show,Atom,$anon$1@76a4d6c)

scala> Service.print(s1, s1.tp, s1.en)
res1: String = some show

Upvotes: 3

Related Questions