-
-
Save channingwalton/2639097 to your computer and use it in GitHub Desktop.
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))) | |
} |
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. :-)
I would be interested in a general solution, but failing that, this specific case would still be useful
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. :-(
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.
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.
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?
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.
I think missingfaktor has cracked it! It is an example of the expression problem.
:-) Glad it helped.
Here is a Haskell snippet, fwiw: http://ideone.com/paWv1.
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.
That problem is kinda-sorta solved here: http://stackoverflow.com/questions/7213676/forall-in-scala.
Thats very interesting thanks :)
I'm struggling to use that technique on this example but I'll persevere.
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
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.)
Oh, I think I got it: https://gist.github.com/2656500. :-) But I am unable to translate it to Scala. :-(
Nice to see how terse the Haskell is
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.
There must be a better way to do this!