-
Notifications
You must be signed in to change notification settings - Fork 97
Serialization
Scoobi provides excellent serialization support for both built-in types, as well as custom types. The trait WireFormat is used for everything that can be serialized. A sensible implementation exists for most basic types (Ints, Floats, Strings, Lists, Option etc.) and there's even an implicit conversion for anything that implements java.io.Serializable.
Because Scala's case classes automatically implement java.io.Serializable you'll likely never have to worry about Serialization. However the astute reader will know that relying on java.io.Serializable isn't always the best idea as it's not very fast, it's quite space inefficient and it's not particularly typesafe (e.g. Put something that's not serializable in a case class, and it'll fail at runtime).
Scoobi solves this with a set of functions in the object WireFormat, to allow you to override the fallback on java.io.Serializable with an implementation that is faster, smaller and more typesafe.
This is the simplest, and easiest to understand. If you have an Object that you want to Serialize, as in:
Object NotYetProcessed extends ProcessedState { /* some code */ }
(This is often useful when constructing ASTs.) To make this efficiently serializable, you would write:
implicit val NotYetProcessedConversion = mkObjectWireFormat(NotYetProcessed)
This calls the function "mkObjectWireFormat" which provides a (perfectly) efficient implementation for reading/writing this. By assigning it to an implicit val, the Scala compiler will automatically use this type when for it needs a WireFormat[NotYetProcessed] (as is the case when using DLists of them). Take a look at the examples for a full example if this isn't clear.
This is designed to provided increased type-saftey while being far more efficient than java.io.Serializable. It is primarily for case-classes (hence the name) but can be used for non-case classes with a bit extra work.
The default java.io.Serializable implementation uses an extensible format with information about the object and type that we don't need (as we always know the type when deserializing). So we only want to serialize the (useful) members of a Class. The function mkCaseWireFormat[T] does this, by taking two functions: A function that can construct a T, and a function that can deconstruct a T. Then these values are (recursively) serialized.
Fortunately we don't need to write the "construct" and "deconstruct" functions ourselves, as Scala has already created it for a case class. They are called "apply" and "unapply" in scala. So given a case class:
case class Rectangle(width: Int, height: Double)
We could create an efficient format by simply going:
implicit val RectangleFormat = mkWireFormat(Rectangle, Rectangle.unapply _)
The first argument is "apply" function, and the second argument is partially applied unapply function. This function will make sure (at compile time) all the types (in this case, an Int and a Double) can be serialized and will serialize this type with no extra fat. It will literally write the Int, then write the Double. Awesome!
Quite often you might have something that's a bit like this (taken from the shortestPath example):
sealed abstract class Progress()
Object Unprocessed extends Progress
case class Frontier(best: Int) extends Progress
case class Done(best: Int) extends Progress
We already know how to specify an efficient scheme for Unprocessed / Frontier / Done. But what if want to be able to efficiently serialize the abstract base class (Progress). This can be achieved with mkAbstractWireFormat. As the first type parameter we specify what is the Abstract type we want to create an efficient encoding for, and the subsequent arguments are all the "leaf" classes it could possibly be.
implicit val progressFormat = mkAbstractWireFormat[Progress, Unprocessed, Frontier, Done]
Now when we try to Serialize an object of type "Progress", it will figure out what is the actual type is, serialize an identifier for that, and then proceed to serialize that type.