6
votes

Say I have two traits that I would like to mixin to a class. The traits each implement an abstract method that the class needs.

trait Writable {
    def serialize(out: java.io.DataOutput)
}

trait T1 extends Writable

trait A extends T1 {
   val aNum: Int
   abstract override def serialize(out: java.io.DataOutput) = {
       super.serialize(out)
       println("A serialize")
       out.writeInt(aNum)
   }

   def action = println("A action")
}

trait B extends T1 {
   val bNum: Int
   abstract override def serialize(out: java.io.DataOutput) = {
       super.serialize(out)
       println("B serialize")
       out.writeInt(bNum)
   }

   def action = println("B action")
}

abstract class M[CT1 <: T1](val mNum: Int) extends Writable {
   this: M[CT1] with T1 =>
   def serialize(out: java.io.DataOutput) = {
       println("M serialize")
       out.writeInt(mNum)
   }

   def action
}

I can then construct a concrete M with either A or B and serialize:

scala> val m1 = new M[A](10) with A { val aNum = 20 }
m1: M[A] with A = $anon$1@67c1e630

scala> val m2 = new M[B](20) with B { val bNum = 30 }
m2: M[B] with B = $anon$1@c36f58e

scala> val out = new java.io.DataOutputStream(new java.io.ByteArrayOutputStream())
out: java.io.DataOutputStream = java.io.DataOutputStream@87afebf

scala> m1.serialize(out)
M serialize
A serialize

scala> m2.serialize(out)
M serialize
B serialize

Everything works as expected. But how do I deserialize the objects while respecting the type of trait that is mixed into M? I could output the trait's name in the serialize method and then have M's deserialize method dispatch on the name but what if I have classes other than M that A and B can be mixed into? Then, each class would have to duplicate the behavior of M's dispatching deserialize. The problem gets even worse if I have multiple traits that need to get mixed into an object to make it concrete and each has it's own custom serialization/deserialization to do. Anyone tackle an issue like this?

1

1 Answers

5
votes

Yes, people have. The way to go is to use typeclass pattern championed by David MacIver's sbinary and Debasish Ghosh's sjson. Debasish's trilogy

are especially useful to all intermediate Scala programmers.

Today, many of the libraries are adopting this methodology including mine scalaxb. See

I've borrowed the naming idea from Scala Collections' CanBuildFrom and named my typeclasses as follows:

trait CanReadXML[A] {
  def reads(seq: scala.xml.NodeSeq): Either[String, A]
}

trait CanWriteXML[A] {
  def writes(obj: A, namespace: Option[String], elementLabel: Option[String],
      scope: NamespaceBinding, typeAttribute: Boolean): NodeSeq
}

trait XMLFormat[A] extends CanWriteXML[A] with CanReadXML[A]

Edit:

Could you explain to me how the framework chooses between "with A" or "with B"?

Using the typeclass pattern, the libraries mixes in neither A nor B. To take scalaxb for example, it provides a method called scalaxb.fromXML in the package object defined as follows:

def fromXML[A](seq: NodeSeq, stack: List[ElemName] = Nil)
              (implicit format: XMLFormat[A]): A = format.reads(seq, stack) match {
  case Right(a) => a
  case Left(a) => throw new ParserFailure(a)
}

Given that you have XML document, and you want to unmarshal (deserialize) it to ipo.Address object, you would call

scalaxb.fromXML[ipo.Address](<shipTo xmlns="http://www.example.com/IPO">
  <name>Foo</name>
  <street>1537 Paper Street</street>
  <city>Wilmington</city>
</shipTo>)

The Address object stays pure using the typeclass pattern:

case class Address(name: String, street: String, city: String)

How does the compiler know what to do? The magic is the implicit parameter required by fromXML called implicit format: XMLFormat[A]. This requires that you have XMLFormat[Address] available as an implicit value within the scope where scalaxb.fromXML[ipo.Address](...) is called.

This made available in the code generated by scalaxb because it mixes in XMLProtocol into the package object of ipo package. And ipo.XMLProtocol defines

implicit lazy val IpoAddressFormat: scalaxb.XMLFormat[ipo.Address] = new DefaultIpoAddressFormat {}

Edit2:

I think I am starting to understand the actual question. You have an object consisting of trait mixins, and you want to somehow "deserialize" the trait composition on other process. As you wrote, you could include some tag for each traits, and load whatever you can.

Since I've written so far on typeclass pattern, let me continue with the approach. The neat thing about having serialization code outside of the object is that you could actually describe mixin combination of the object. Suppose there are traits

trait Foo { def foo: Int }
trait Bar { def bar: Int }

and you want to describe the mixin as <obj><foo>1</foo><bar>2</bar></obj>. Here's a gist I whipped up. I defined typeclass instance for Foo, Bar, and Foo with Bar, and called

Def.fromXML[Foo with Bar](<obj><foo>1</foo><bar>2</bar></obj>)

which returned

Right(FooWithBar(1, 2))