NovelIO


Pickler Combinators

Pickler combinators are a concept described by Andrew J. Kennedy (http://research.microsoft.com/pubs/64036/picklercombinators.pdf).

Their purpose is to present a serialisation/deserialiation mechanism that grants the developer explicit control over their serialisation format while avoiding the requirement to write lots of tedious, error prone code.

Pickler combinators operate by allowing the construction of pickler/unpickler pairs (hereafter called PUs from brevity) which excapsulate the serialisation process.

PU primitives

PU primitives are provided to handle simple data types and more complicated PUs can be constructed by combining these PUs using combinator functions.

let intPU = BinaryPickler.intPU

let floatPU = BinaryPickler.float32PU

let asciiPU = BinaryPickler.asciiPU

These are just a few examples of the primitive PUs defined in this library. These PUs can be used to serialise (pickle) or deserialise (unpickle) values of the type associated with their PU to and from their binary representations.

The beauty of pickler combinators is that the same PU can be used to transform data in both directions.

Endianness

The default PUs for each datatype, such as intPU will pickle in the endianness of the current platform. It's also possible to specify endianness for each primitive.

let littleEndianIntPU = BinaryPickler.LittleEndian.intPU // little endian int PU

let bigEndianUtf32PU = BinaryPickler.BigEndian.utf32PU // big endian utf-32 PU

Little endian PUs are found in the LittleEndian submodule and big endian PUs are found in the BigEndian submodule. Note that both of these modules also contain list and array combinator PUs, that's because these combinators prefix a length to the output and that length needs a defined endianness.

Running PUs

let bytes = BinaryPickler.pickle (BinaryPickler.intPU) 64 // convert the int value 64 into a byte array

let intR = BinaryPickler.unpickle (BinaryPickler.intPU) bytes // convert the byte array back into an int

Here we use the pickle function to transform the int 64 into its binary representation and then transform it back using the unpickle function.

Tuple combinators

A variety of tuple combinators are provided to allow the pickling/unpickling of tuples. These can be constructed, for example, as follows:

let intUtf8PU = BinaryPickler.tuple2 BinaryPickler.intPU BinaryPickler.utf8PU

The tuple2 function is simply supplied with two PUs and it constructs a combined pickler.

tuple3 and tuple4 functions are also provided, allowing the construction of more complex PUs.

Wrapping PUs

Rather than constructing data from tuples, we may wish to pickle/unpickle custom data types. It is possible to do this by providing a function which constructs and deconstructs this custom data-type.

/// Unit of pounds sterling
[<Measure>] type GBP

/// A product with an associated price
type Product = {ProductName : string; ProductPrice : decimal<GBP>}

/// A pickler/unpickler pair (PU) for products
let productPU =
    let nameDecPU = BinaryPickler.tuple2 BinaryPickler.utf8PU BinaryPickler.decimalPU
    let toProd (name,price) = {ProductName = name; ProductPrice = price*1.0M<GBP>} // tuple to product
    let fromProd prod = prod.ProductName, decimal prod.ProductPrice // product to tuple
    BinaryPickler.wrap (toProd, fromProd) nameDecPU

Here, the custom record type contains a string and a decimal with a unit of measure so we define a tuple PU which will pickle/unpickle the underlying data and provide functions that construct and deconstruct data from that form.

Data can then easily be read into or written from our custom data type.

List and Array combinators

The list and array combinators take a PU of type 'a and produce a pickler that pickles the corresponding collection type.

let intArrayPU = BinaryPickler.array BinaryPickler.intPU

let floatListPU = BinaryPickler.list BinaryPickler.intPU

It is, of course, possible to combine several of these combinators to produce complicated list PUs, e.g.:

let complexPickler = 
    BinaryPickler.list 
        (BinaryPickler.tuple3 
            BinaryPickler.asciiPU BinaryPickler.floatPU BinaryPickler.intPU)

Fixed length string encodings

Fixed length strings can be encoded either in bulk, as a complete string, or character by character. Take ASCII, for example:

let asciiStringPU = BinaryPickler.asciiPU

let asciiCharPU = BinaryPickler.asciiCharPU

Char PUs can also be combined using the 'lengthPrefixed' and 'nullTerminated' combinators to build derived string PUs.

let lengthPrefixedAscii = BinaryPickler.lengthPrefixed BinaryPickler.asciiCharPU

let nullTermAscii = BinaryPickler.nullTerminated BinaryPickler.asciiCharPU

The 'lengthPrefixed' combinator encodes the number of characters in the string before the list of characters while the 'nullTerminated' encodes a 'NULL' character as the final item in the string.

Variable length string encodings

Variable length strings (such as UTF-8) cannot be encoded character by character so only the bulk string option exists.

let utf8PU = BinaryPickler.utf8PU

Encoding Discriminated Unions (the alt combinator)

Consider a simple data type:

type Shape =
    |Circle of float
    |Rectangle of float * float

let shapePU =
    // create a pickler for the circle and recangle case, wrap takes a method of constructing and deconstructing each case
    let circlePU = BinaryPickler.wrap (Circle, function Circle r -> r) BinaryPickler.floatPU
    let rectanglePU = BinaryPickler.wrap (Rectangle, function Rectangle (w, h) -> w, h) (BinaryPickler.tuple2 BinaryPickler.floatPU BinaryPickler.floatPU)
    // a tag map : 0 -> circle, 1 -> rectangle defining which PU to use for which tag
    let altMap = Map.ofList [(0, circlePU); (1, rectanglePU)]
    // use the alt combinator and the deconstruction of Shape to the tags defined above
    BinaryPickler.alt (function | Circle _ -> 0 | Rectangle _ -> 1) altMap

The alt combinator is the key to this process. It accepts a function that deconstructs a data type into a simple numeric tag and a Map which defines the PU to use internally for each of the cases.

Encoding Recursive Values

Since F# is an eagerly evaluated language, we cannot define recursive values as they would never resolve. To avoid this problem, a RecursivePU constructor is provided to allow the recursive definition of the PU to be deferred until required.

A good example of a suitable data type is provided in the paper:

type Bookmark =
    |URL of string
    |Folder of string * Bookmark list

We can define a PU for this type by using a mutally recusive value and a function in combination with the RecursivePU constructor.

let rec bookmarkPU = RecursivePU bookmarkPURec
and private bookmarkPURec() =
    // define a PU for the URL case, this is just a UTF-8 PU with a way of constructing and deconstructing a Bookmark
    let urlPU = BinaryPickler.wrap (URL, function URL x -> x) BinaryPickler.utf8PU
    // a pickler for the folder case is a tuple2 PU with a UTF-8 PU for the name and a list pickler of bookmarkPU's and a way of constructing
    // and deconstructing the Bookmark
    let folderPU = BinaryPickler.wrap (Folder, function Folder (st, bms) -> st, bms) (BinaryPickler.tuple2 BinaryPickler.utf8PU (BinaryPickler.list bookmarkPU))
    // define that tag 0 means urlPU and tag 1 means folderPU
    let m = Map.ofList [(0, urlPU);(1, folderPU)]
    // define that URL should mean use tag 0 and Folder should mean use tag 1
    m |> BinaryPickler.alt (function | URL _ -> 0 | Folder _ -> 1)

This approach permits the pickling/unpickling of potentially very complex data types with very little development work required.

Incremental Pickling

In many cases, especially when dealing with large binary files, it could be desirable to not have to convert back and forth between extremely large byte arrays, indeed this approach might not be viable due to available memory.

In this case, we can use incremental pickling to read/write as part of the pickling process. Unlike the simple conversion process shown above, this action is effectful so is encapsulated within IO.

This process is quite simple, instead of using the pickle and unpickle functions, we use the pickleIncr and unpickleIncr functions. These simply take the additional argument of a BChannel upon which they will act.

Example of incremental unpickling:

io {
    let! channel = File.openBinaryChannel FileMode.Open FileAccess.Read (File.assumeValidFilename "test.txt")
    return! BinaryPickler.unpickleIncr complexPickler channel
}

Example of incremental pickling:

io {
    let! channel = File.openBinaryChannel FileMode.Create FileAccess.Write (File.assumeValidFilename "test.txt")
    let data = [("A", 7.5, 16); ("B", 7.5, 1701)]
    return! BinaryPickler.pickleIncr complexPickler channel data
}
namespace NovelFS
namespace NovelFS.NovelIO
namespace NovelFS.NovelIO.BinaryPickler
val intPU : BinaryPU<int32>

Full name: Pickler.intPU
Multiple items
module BinaryPickler

from NovelFS.NovelIO.BinaryPickler

--------------------
namespace NovelFS.NovelIO.BinaryPickler
val intPU : BinaryPU<int32>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.intPU
val floatPU : BinaryPU<float32>

Full name: Pickler.floatPU
val float32PU : BinaryPU<float32>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.float32PU
val asciiPU : BinaryPU<string>

Full name: Pickler.asciiPU
val asciiPU : BinaryPU<string>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.asciiPU
val littleEndianIntPU : BinaryPU<int32>

Full name: Pickler.littleEndianIntPU
module LittleEndian

from NovelFS.NovelIO.BinaryPickler.BinaryPickler
val intPU : BinaryPU<int32>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.LittleEndian.intPU
val bigEndianUtf32PU : BinaryPU<string>

Full name: Pickler.bigEndianUtf32PU
module BigEndian

from NovelFS.NovelIO.BinaryPickler.BinaryPickler
val utf32PU : BinaryPU<string>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.BigEndian.utf32PU
val bytes : byte []

Full name: Pickler.bytes
val pickle : pu:BinaryPU<'a> -> value:'a -> byte []

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.pickle
val intR : int32

Full name: Pickler.intR
val unpickle : pu:BinaryPU<'a> -> array:byte array -> 'a

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.unpickle
val intUtf8PU : BinaryPU<int32 * string>

Full name: Pickler.intUtf8PU
val tuple2 : pa:BinaryPU<'a> -> pb:BinaryPU<'b> -> BinaryPU<'a * 'b>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.tuple2
val utf8PU : BinaryPU<string>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.utf8PU
Multiple items
type MeasureAttribute =
  inherit Attribute
  new : unit -> MeasureAttribute

Full name: Microsoft.FSharp.Core.MeasureAttribute

--------------------
new : unit -> MeasureAttribute
[<Measure>]
type GBP

Full name: Pickler.GBP


 Unit of pounds sterling
type Product =
  {ProductName: string;
   ProductPrice: decimal<GBP>;}

Full name: Pickler.Product


 A product with an associated price
Product.ProductName: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
Product.ProductPrice: decimal<GBP>
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.decimal

--------------------
type decimal = System.Decimal

Full name: Microsoft.FSharp.Core.decimal

--------------------
type decimal<'Measure> = decimal

Full name: Microsoft.FSharp.Core.decimal<_>
val productPU : BinaryPU<Product>

Full name: Pickler.productPU


 A pickler/unpickler pair (PU) for products
val nameDecPU : BinaryPU<string * System.Decimal>
val decimalPU : BinaryPU<System.Decimal>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.decimalPU
val toProd : (string * decimal -> Product)
val name : string
val price : decimal
val fromProd : (Product -> string * decimal)
val prod : Product
val wrap : fab:('a -> 'b) * fba:('b -> 'a) -> pa:BinaryPU<'a> -> BinaryPU<'b>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.wrap
val intArrayPU : BinaryPU<int32 []>

Full name: Pickler.intArrayPU
val array : pa:BinaryPU<'a> -> BinaryPU<'a []>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.array
val floatListPU : BinaryPU<int32 list>

Full name: Pickler.floatListPU
val list : pa:BinaryPU<'a> -> BinaryPU<'a list>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.list
val complexPickler : BinaryPU<(string * float * int32) list>

Full name: Pickler.complexPickler
val tuple3 : pa:BinaryPU<'a> -> pb:BinaryPU<'b> -> pc:BinaryPU<'c> -> BinaryPU<'a * 'b * 'c>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.tuple3
val floatPU : BinaryPU<float>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.floatPU
val asciiStringPU : BinaryPU<string>

Full name: Pickler.asciiStringPU
val asciiCharPU : BinaryPU<char>

Full name: Pickler.asciiCharPU
val asciiCharPU : BinaryPU<char>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.asciiCharPU
val lengthPrefixedAscii : BinaryPU<string>

Full name: Pickler.lengthPrefixedAscii
val lengthPrefixed : pa:BinaryPU<char> -> BinaryPU<string>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.lengthPrefixed
val nullTermAscii : BinaryPU<string>

Full name: Pickler.nullTermAscii
val nullTerminated : pa:BinaryPU<char> -> BinaryPU<string>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.nullTerminated
val utf8PU : BinaryPU<string>

Full name: Pickler.utf8PU
type Shape =
  | Circle of float
  | Rectangle of float * float

Full name: Pickler.Shape
union case Shape.Circle: float -> Shape
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = System.Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
union case Shape.Rectangle: float * float -> Shape
val shapePU : BinaryPU<Shape>

Full name: Pickler.shapePU
val circlePU : BinaryPU<Shape>
val r : float
val rectanglePU : BinaryPU<Shape>
val w : float
val h : float
val altMap : Map<int,BinaryPU<Shape>>
Multiple items
module Map

from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> =
  interface IEnumerable
  interface IComparable
  interface IEnumerable<KeyValuePair<'Key,'Value>>
  interface ICollection<KeyValuePair<'Key,'Value>>
  interface IDictionary<'Key,'Value>
  new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
  member Add : key:'Key * value:'Value -> Map<'Key,'Value>
  member ContainsKey : key:'Key -> bool
  override Equals : obj -> bool
  member Remove : key:'Key -> Map<'Key,'Value>
  ...

Full name: Microsoft.FSharp.Collections.Map<_,_>

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val ofList : elements:('Key * 'T) list -> Map<'Key,'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Map.ofList
val alt : tag:('a -> int32) -> ps:Map<int32,BinaryPU<'a>> -> BinaryPU<'a>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.alt
type Bookmark =
  | URL of string
  | Folder of string * Bookmark list

Full name: Pickler.Bookmark
union case Bookmark.URL: string -> Bookmark
union case Bookmark.Folder: string * Bookmark list -> Bookmark
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
val bookmarkPU : BinaryPU<Bookmark>

Full name: Pickler.bookmarkPU
union case BinaryPU.RecursivePU: (unit -> BinaryPU<'a>) -> BinaryPU<'a>
val private bookmarkPURec : unit -> BinaryPU<Bookmark>

Full name: Pickler.bookmarkPURec
val urlPU : BinaryPU<Bookmark>
val x : string
val folderPU : BinaryPU<Bookmark>
val st : string
val bms : Bookmark list
val m : Map<int,BinaryPU<Bookmark>>
val io : IO.IOBuilder

Full name: NovelFS.NovelIO.IOBuilders.io
val channel : BChannel
module File

from NovelFS.NovelIO
val openBinaryChannel : options:FileOpenOptions -> fName:FilePath -> IO<BChannel>

Full name: NovelFS.NovelIO.File.openBinaryChannel
type FileMode = System.IO.FileMode

Full name: NovelFS.NovelIO.FileMode
field System.IO.FileMode.Open = 3
type FileAccess = System.IO.FileAccess

Full name: NovelFS.NovelIO.FileAccess
field System.IO.FileAccess.Read = 1
val unpickleIncr : pu:BinaryPU<'a> -> binaryChannel:BChannel -> IO<'a>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.unpickleIncr
field System.IO.FileMode.Create = 2
field System.IO.FileAccess.Write = 2
val data : (string * float * int) list
val pickleIncr : pu:BinaryPU<'a> -> binaryChannel:BChannel -> value:'a -> IO<unit>

Full name: NovelFS.NovelIO.BinaryPickler.BinaryPickler.pickleIncr
Fork me on GitHub