Skip to content

Instantly share code, notes, and snippets.

@channingwalton
Created May 8, 2012 20:39
Show Gist options
  • Save channingwalton/2639097 to your computer and use it in GitHub Desktop.
Save channingwalton/2639097 to your computer and use it in GitHub Desktop.
Typeclass in the presence of subtypes
import scala.xml.NodeSeq
object RenderExample {
object Model {
trait Toy
case class Bike extends Toy
case class Train extends Toy
case class Address(number: Int, street: String, postcode: String)
case class Person(name: String, age: Int, address: Address, toy: Toy)
}
object Rendering {
import Model._
trait ExEmEl[A] {
def xml(a: A): NodeSeq
}
object ExEmEl {
implicit object intExEmEl extends ExEmEl[Int] { def xml(i: Int) = <anInt>{ i }</anInt> }
implicit object bikeExEmEl extends ExEmEl[Bike] { def xml(toy: Bike) = <bike/> }
implicit object trainExEmEl extends ExEmEl[Train] { def xml(toy: Train) = <train/> }
implicit def toyExEmEl(implicit b: ExEmEl[Bike], t: ExEmEl[Train]) = new ExEmEl[Toy] {
def xml(toy: Toy) = toy match {
case bike: Bike ⇒ b.xml(bike)
case train: Train ⇒ t.xml(train)
case _ ⇒ NodeSeq.Empty // NO NO NO
}
}
implicit def personToXml(implicit toy: ExEmEl[Toy], add: ExEmEl[Address]) = new ExEmEl[Person] {
def xml(person: Person) =
<person>
<name>{ person.name }</name>
<age>{ person.age }</age>
{ add.xml(person.address) }
{ toy.xml(person.toy) }
</person>
}
implicit def addressToXml: ExEmEl[Address] = new ExEmEl[Address] {
def xml(address: Address) =
<address>
<number>{ address.number }</number>
<street>{ address.street }</street>
<postcode>{ address.postcode }</postcode>
</address>
}
}
object ExEmElRenderer {
def render[A: ExEmEl](value: A) = implicitly[ExEmEl[A]].xml(value)
}
}
}
object Test extends App {
import RenderExample._
import Rendering._
import Model._
val toy: Toy = Bike()
println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), toy)))
println(ExEmElRenderer.render(3))
// add a new Toy
case class Ball extends Toy
val ball: Toy = Ball()
println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), ball)))
implicit object ballExEmEl extends ExEmEl[Ball] { def xml(toy: Ball) = <ball/> }
implicit def toyExEmEl(implicit ball: ExEmEl[Ball]) = new ExEmEl[Toy] {
def xml(toy: Toy) = toy match {
case b: Ball ⇒ ball.xml(b)
case other ⇒ ExEmElRenderer.render(other)
}
}
println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), ball)))
println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), toy)))
}
@channingwalton
Copy link
Author

There must be a better way to do this!

@missingfaktor
Copy link

Do you want a solution for this specific problem or the problem of 'type-classes under subtyping' in general? I think I can help with the former. Latter, perhaps not. :-)

@channingwalton
Copy link
Author

I would be interested in a general solution, but failing that, this specific case would still be useful

@missingfaktor
Copy link

Never mind, and sorry. The solution I had on mind does not quite fit here. I tried something else too, but it incurs just as much boilerplate. :-(

@etorreborre
Copy link

One approach could be to be more specific in the type of Toy you assign to Person:

case class Person[T <: Toy](name: String, age: Int, address: Address, toy: T)

Then you define the rendering implicit for Person like that:

implicit def personToXml[T <: Toy](implicit add: ExEmEl[Address], t: ExEmEl[T]): ExEmEl[Person[T]] = new ExEmEl[Person[T]] {
    def xml(person: Person[T]) =
      <person>
        <name>{ person.name }</name>
        <age>{ person.age }</age>
        { add.xml(person.address) }
        { t.xml(person.toy) }
      </person>
  }

And you don't need to define the awkward Toy renderer implicit with an empty default case.

@channingwalton
Copy link
Author

Actually that doesn't work either Eric, the following fails to compile:

val toy: Toy = Bike()
println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), toy)))

could not find implicit value for evidence parameter of type RenderExample.Rendering.ExEmEl[RenderExample.Model.Person[RenderExample.Model.Toy]] RenderExample.scala line 72

If you change the val toy: Toy to val toy or val toy: Bike then its ok.

@etorreborre
Copy link

You're right, I actually left out the type annotation... I'll try to give this problem more thought, I think it's bound to happen in many use of typeclasses. Maybe it's worth posting a barebones version on the Scala mailing-list or on Stackoverflow?

@missingfaktor
Copy link

You are experiencing expression problem. (http://c2.com/cgi/wiki?ExpressionProblem.) Subtyping makes it easier to add new types, but harder to add new operations. ADTs make it easier to add new operations, but harder to add new types. With type-classes, both adding new types and operations is equally easy (excusing the boilerplate typeclasses incur in Scala). Keeping that in mind, I have made some changes. See below:

import scala.xml.NodeSeq

object RenderExample {

  object Model {
    trait IsAToy[A]

    object Bike
    implicit object BikeIsAToy extends IsAToy[Bike.type]

    object Train
    implicit object TrainIsAToy extends IsAToy[Train.type]

    case class Address(number: Int, street: String, postcode: String)
    case class Person[T : IsAToy](name: String, age: Int, address: Address, toy: T)
  }

  object Rendering {
    import Model._

    trait ExEmEl[A] {
      def xml(a: A): NodeSeq
    }

    object ExEmEl {

      implicit object intExEmEl extends ExEmEl[Int] { def xml(i: Int) = <anInt>{ i }</anInt> }

      implicit object bikeExEmEl extends ExEmEl[Bike.type] { def xml(toy: Bike.type) = <bike/> }

      implicit object trainExEmEl extends ExEmEl[Train.type] { def xml(toy: Train.type) = <train/> }

      implicit def personToXml[T](implicit isAToy: IsAToy[T], toy: ExEmEl[T], add: ExEmEl[Address]) = new ExEmEl[Person[T]] {
        def xml(person: Person[T]) =
          <person>
            <name>{ person.name }</name>
            <age>{ person.age }</age>
            { add.xml(person.address) }
            { toy.xml(person.toy) }
          </person>
      }

      implicit def addressToXml: ExEmEl[Address] = new ExEmEl[Address] {
        def xml(address: Address) =
          <address>
            <number>{ address.number }</number>
            <street>{ address.street }</street>
            <postcode>{ address.postcode }</postcode>
          </address>
      }
    }

    object ExEmElRenderer {
      def render[A: ExEmEl](value: A) = implicitly[ExEmEl[A]].xml(value)
    }
  }
}

object Main {
  import RenderExample._
  import Rendering._
  import Model._

  def main(args: Array[String]) {
    println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), Bike)))
    println(ExEmElRenderer.render(3))

    // add a new Toy
    object Ball
    implicit object BallIsAToy extends IsAToy[Ball.type]
    implicit object ballExEmEl extends ExEmEl[Ball.type] { def xml(toy: Ball.type) = <ball/> }

    println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), Ball)))
    println(ExEmElRenderer.render(Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), Bike)))
  }
}

Let me know if that helps.

@channingwalton
Copy link
Author

I think missingfaktor has cracked it! It is an example of the expression problem.

@missingfaktor
Copy link

:-) Glad it helped.

Here is a Haskell snippet, fwiw: http://ideone.com/paWv1.

@channingwalton
Copy link
Author

The problem here is that I want to a a reference to a Person or List[Person], but I don't know the specific type of their toys.
eg.

val person1: Person[_] = Person("Angelica", 4, Address(14, "Orchid Drive", "GU24 9SB"), Ball)

I don't know if thats possible but its the most common use case.

@missingfaktor
Copy link

That problem is kinda-sorta solved here: http://stackoverflow.com/questions/7213676/forall-in-scala.

@channingwalton
Copy link
Author

Thats very interesting thanks :)

@channingwalton
Copy link
Author

I'm struggling to use that technique on this example but I'll persevere.

@channingwalton
Copy link
Author

Ok, here is a solution using the ideas that Miles suggested in http://stackoverflow.com/questions/7213676/forall-in-scala.

import scala.xml.NodeSeq

object RenderExample {

  object Rendering {

    trait ExEmEl[A] {
      def xml(a: A): NodeSeq
    }

    object ExEmElRenderer {
      def render[A: ExEmEl](value: A) = implicitly[ExEmEl[A]].xml(value)
    }

  }

  object Model {
    import Rendering._

    trait IsAToy

    case class Bike extends IsAToy

    case class Train extends IsAToy

    case class Person[T <: IsAToy](name: String, toy: T)(implicit val toyExEmEl: ExEmEl[T])

    implicit object bikeExEmEl extends ExEmEl[Bike] { def xml(toy: Bike) = <bike/> }

    implicit object trainExEmEl extends ExEmEl[Train] { def xml(toy: Train) = <train/> }

    def renderPerson(person: Person[_]) = person match {
      case p @ Person(n, t) 
        <person>
          <name>{ p.name }</name>
          { p.toyExEmEl.xml(p.toy) }
        </person>
    }
  }
}

object Test extends App {
  import RenderExample._
  import Rendering._
  import Model._
  import ExEmElRenderer._

  val angelica: Person[_] = Person("Angelica", Bike())
  println(renderPerson(angelica))

  // can we add a new Toy
  case class Ball extends IsAToy
  implicit object ballExEmEl extends ExEmEl[Ball] { def xml(toy: Ball) = <ball/> }

  val person1 = Person("Angelica", Ball())
  println(renderPerson(person1))
}

But this is not satisfactory. We want a typeclass available in some library, then make use of that typeclass (our Person) and have it be supported by the original library. This solution does not do that, a new method had to be introduced

@missingfaktor
Copy link

Sorry. Actually, the discussion in the thread is about how to create a list of values depending on the common interface they implement. So with the type class scheme I showed earlier, you can have a List[Those with ExEmEl instance] but not List[Person[_]]. I am not sure though. Perhaps Miles or Runar can help.

Anyway, here is the Haskell code to illustrate what I said above: https://gist.github.com/2656460. (The reason I didn't write it in Scala is because it gets pretty hairy there, as can be seen in that thread, and I can't write it without looking up Runar's answer.)

@missingfaktor
Copy link

Oh, I think I got it: https://gist.github.com/2656500. :-) But I am unable to translate it to Scala. :-(

@channingwalton
Copy link
Author

Nice to see how terse the Haskell is

@channingwalton
Copy link
Author

The other problem with the final solution above is that the type class instance is fixed at the time the object is constructed which is invariably not where you want the typeclass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment