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

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