Daily Coding Problem 3
Problem Statement
This problem was asked by Google.
Given the root to a binary tree, implement serialize(root), which serializes the tree into a string, and deserialize(s), which deserializes the string back into the tree.
For example, given the following Node class
class Node:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
The following test should pass:
node = Node('root', Node('left', Node('left.left')), Node('right'))
assert deserialize(serialize(node)).left.left.val == 'left.left'
Solution
Each solution requires two algorithms, serialize and deserialize.
First, let's pull in the Nuget packages for the solution.
// Easy way to work with any Nuget packages. Standard packages to load, regardless of solution.
#load "Paket.fsx"
Paket.Version [
    "FSharp.Core", "4.5.4"
    "FParsec", "1.0.4-RC3"
]
Paket.Package [
    "Expecto"
    "Xplot.Plotly"
    "XPlot.GoogleCharts"
    "Newtonsoft.Json"
    "Chiron"
]
#load "Paket.Generated.Refs.fsx"
#load "XPlot.GoogleCharts.fsx"
// Load the DLLs
#r "packages/Expecto/lib/net461/Expecto.dll"
#r "packages/FParsec/lib/portable-net45+win8+wp8+wpa81/FParsec.dll"
#r "packages/FParsec/lib/portable-net45+win8+wp8+wpa81/FParsecCS.dll"
#r "packages/Newtonsoft.Json/lib/net45/Newtonsoft.Json.dll"
#r "packages/Chiron/lib/net452/Chiron.dll"
/// Quick debugging
let tee f x = f x |> ignore; x
Strategy Part 1 - Pick the correct data model
The strategy is to provide behavior in F# that is similar to Python (at least, as much as possible given the only example).  Since val is a keyword in F#, using it as a field name required wrapping it in 2 back-ticks, aka ``val``.  Or renaming it to something like value.
The key here, in my mind, is to pick the appropriate data type to hold the tree. Some choices include:
- Discriminated union
 - Record type
 - Class
 
open System
module Model =
    // Need to define static members to allow Chiron to work
    open Chiron
    open Chiron.Operators
    /// Tree as discriminated union or choice type
    type DUNode =
    | Node of string * DUNode option * DUNode option
    with
        static member create(v, l, r) = Node(v, l, r)
        static member create(v, l) = DUNode.create(v, l, None)
        static member createOpt(v, l, r) = DUNode.create(v, l, r) |> Some
        static member createOpt(v, l) = DUNode.create(v, l, None) |> Some
        static member createOpt(v) = DUNode.create(v, None, None) |> Some
        member x.``val`` = match x with Node(v, l, r) -> v
        member x.left = match x with Node(_, l, _) -> l
        member x.right = match x with Node(_, _, r) -> r
        // Chiron functions
        static member ToJson (Node(v, l, r)) = Json.write "dunode" (v, l, r)
        static member FromJson (_:DUNode) = Json.read "dunode" |> Json.map Node
    /// Tree as record type
    type RNode = {
        ``val``: string
        left: RNode option
        right: RNode option
    } with
        // Convenience methods to construct the record
        static member create(v, l, r) = {``val``=v; left=l; right=r}
        static member createOpt(v, l, r) = RNode.create(v, l, r) |> Some
        static member createOpt(v, l) = RNode.create(v, l, None) |> Some
        static member createOpt(v) = RNode.create(v, None, None) |> Some
        // Chiron functions
        static member ToJson (node:RNode) =
            json {
                do! Json.write "rval" node.``val``
                match node.left with
                | Some(l) -> do! Json.write "rleft" l
                | None -> do! Json.writeNone "rleft"
                match node.right with
                | Some(r) -> do! Json.write "rright" r
                | None -> do! Json.writeNone "rright"
            }
        static member FromJson (_:RNode) =
            json {
                let! v = Json.read "rval"
                let! l = Json.read "rleft"
                let! r = Json.read "rright"
                return RNode.create(v, l, r)
            }
    /// Tree as class type
    type CNode(``val``:string, left:CNode option, right:CNode option) =
        // Save the values from the constructor
        member this.``val`` = ``val``
        member this.left = left
        member this.right = right
        // Short-hand constructors, but as static members
        static member create(v, l, r) = CNode(v, l, r)
        static member createOpt(v, l, r) = CNode.create(v, l, r) |> Some
        static member createOpt(v, l) = CNode.create(v, l, None) |> Some
        static member createOpt(v) = CNode.create(v, None, None) |> Some
        // Chiron functions
        static member ToJson (node:CNode) =
            json {
                do! Json.write "cval" node.``val``
                match node.left with
                | Some(l) -> do! Json.write "cleft" l
                | None -> do! Json.writeNone "cleft"
                match node.right with
                | Some(r) -> do! Json.write "cright" r
                | None -> do! Json.writeNone "cright"
            }
        static member FromJson (_:CNode) =
            json {
                let! v = Json.read "cval"
                let! l = Json.read "cleft"
                let! r = Json.read "cright"
                return CNode(v, l, r)
            }
        // Expecto `equals` comparison requires overrides for classes
        // Using trick from https://stackoverflow.com/a/38932144
        // More on tuple equality is at https://fsharpforfunandprofit.com/posts/tuples/
        override x.GetHashCode() = hash(x.``val``, x.left, x.right)
        override x.Equals(b) =
            match b with
            | :? CNode as c -> x.GetHashCode() = c.GetHashCode()
            | _ -> false
Strategy Part 2 - Figure out how to Serialize and Deserialize
The next question is this - how to serialize and deserialize the object into a string. The problem doesn't specify the output string's format - so it could be XML, JSON, etc. or something custom.
Here are three ideas:
- Use an auto-JSON serializer and deserializer, like Json.NET.
 - Use a manual JSON serializer and deserializer, like Chiron.
 - Use a custom serializer and deserializer, using something like FParsec.
 
Automatic Serialization using Json.NET
First, we write the functions for Automatic Serialization using Json.NET.
/// Uses Json.NET
module AutoSerializer =
    open Newtonsoft.Json
    // Convenience methods
    let deserialize<'a> str = JsonConvert.DeserializeObject<'a> str
    // DUNode
    let serializeDUNode (n:Model.DUNode) = JsonConvert.SerializeObject n
    let deserializeDUNode = deserialize<Model.DUNode>
    // RNode
    let serializeRNode (n:Model.RNode) = JsonConvert.SerializeObject n
    let deserializeRNode = deserialize<Model.RNode>
    // CNode
    let serializeCNode (n:Model.CNode) = JsonConvert.SerializeObject n
    let deserializeCNode = deserialize<Model.CNode>
/// Uses Chiron
module ManualSerializer =
    open Chiron
    // DUNode
    let serializeDUNode (n:Model.DUNode) =
        Json.serialize n
        |> Json.formatWith Chiron.Formatting.JsonFormattingOptions.Compact
    let deserializeDUNode str : Model.DUNode = str |> Json.parse |> Json.deserialize
    // RNode
    let serializeRNode (n:Model.RNode) =
        Json.serialize n
        |> Json.formatWith Chiron.Formatting.JsonFormattingOptions.Compact
    let deserializeRNode str : Model.RNode = str |> Json.parse |> Json.deserialize
    // CNode
    let serializeCNode (n:Model.CNode) =
        Json.serialize n
        |> Json.formatWith Chiron.Formatting.JsonFormattingOptions.Compact
    let deserializeCNode str : Model.CNode = str |> Json.parse |> Json.deserialize
Custom Serialization using FParsec
Finally, we have the functions for Custom Serialization using FParsec.  The strategy here is to serialize nodes with values as Some("val") Child1 Child2 or as None.  We will also be using a pre-order traversal of the tree, i.e. process the parent node, then the left child, and finally the right child.
module CustomSerializer =
    /// Pre-order traversal for serialization, not tail-recursive.
    /// I am using SRTPs here to avoid having to write identical code for the 3 data types.
    /// Advantage of SRTP over higher order function here is that the record's field accessor
    /// matches the `member` constraint - not sure how to do that with functions without
    /// having to declare a custom member function for each of the record's fields.
    let inline serializeNode (node:^a) =
        let rec helper (node:^a) =
            let lans =
                match (^a:(member left : ^a option) (node)) with
                | Some l -> helper l
                | None -> "None"
            let rans =
                match (^a:(member right : ^a option) (node)) with
                | Some r -> helper r
                | None -> "None"
            sprintf "Some(\"%s\") %s %s" (^a:(member ``val`` : string) (node)) lans rans
        helper node
    // Why not go all out?! This is my first time trying FParsec.  FParsec will be used
    // to deserialize the string back into the appropriate type.
    open FParsec
    /// Pre-order traversal for deserialization
    /// Here, using a higher-order function to avoid SRTP.
    let inline deserializeNode (ctor:string * 'a option * 'a option -> 'a) (str:string) : 'a =
        // Basic parser based on our serialization strategy
        // We don't allow double-quotes inside strings - KISS
        let properString =
            pipe3 (pstring "\"") (manySatisfy (fun x -> x <> '\"')) (pstring "\"")
                  (fun x y z -> x + y + z)
        let someValue = spaces >>. pstring "Some(" >>. properString .>> (pstring ")") .>> spaces
        let noneValue = spaces >>. pstring "None" .>> spaces
        let fullParser = many (someValue <|> noneValue)
        /// Parser will tokenize the string and extract the values we care about
        /// If it is a real value, it will retain the quotes as part of the string: "value"
        /// If it is not a value, it will look like: None
        /// This is important because the value itself could be the string "None", and we
        /// must distinguish this from an actual non-value.
        let tokens =
            run fullParser str
            |> function
                | Success(res,_,_) -> res
                | Failure(err,_,_) -> failwith err
        /// Recursive deserializer, not tail recursive. Returns the deserialized value
        /// and the remaining tokens.
        let rec deser (remTokens:string list) : 'a option * string list = 
            match remTokens with
            | hd::tl when hd.StartsWith("N") ->
                None, tl
            | hd::tl when hd.StartsWith("\"") ->
                // Remove the double-quotes from the value
                let v = hd.[1 .. String.length hd - 2]
                let ln, remAfterLeft = deser tl
                let rn, remAfterRight = deser remAfterLeft
                Some (ctor (v, ln, rn)), remAfterRight
            | _ -> failwith "Tree structure is corrupted"
        // Call the deserializer, then return the data structure
        // NOTE: Option.get should never fail because, if you start from a tree object
        // instead of some contrived string representation, it is impossible to
        // create a tree without a root.
        deser tokens
        |> fst
        |> Option.get
    // DUNode
    let serializeDUNode (n:Model.DUNode) = serializeNode n
    let deserializeDUNode str = deserializeNode (Model.DUNode.create) str
    // RNode
    let serializeRNode (n:Model.RNode) = serializeNode n
    let deserializeRNode str = deserializeNode (Model.RNode.create) str
    // CNode
    let serializeCNode (n:Model.CNode) = serializeNode n
    let deserializeCNode str = deserializeNode (Model.CNode.create) str
Model.DUNode.create("root", Model.DUNode.createOpt("1 left"))
|> tee (printfn "I start as a DUNode:%s%O" Environment.NewLine)
|> CustomSerializer.serializeDUNode
|> tee (printfn "serialized: %O")
|> CustomSerializer.deserializeDUNode
|> tee (printfn "Back to a DUNode:%s%O" Environment.NewLine)
|> CustomSerializer.serializeDUNode
|> tee (printfn "serialized: %O")
|> CustomSerializer.deserializeRNode
|> tee (printfn "Now I'm a RNode:%s%O" Environment.NewLine)
|> CustomSerializer.serializeRNode
|> tee (printfn "serialized: %O")
|> CustomSerializer.deserializeCNode
|> tee (printfn "And now I'm a CNode:%s%O" Environment.NewLine)
Strategy Part 3 - Make sure the De-/Serialization strategies work as expected
Now that we have the 3 sets of de-/serializers written for each of the 3 data types, let's write some basic unit tests to ensure that the functions are behaving as expected.
NOTE: I cannot seem to get FsCheck to run in IfSharp / Jupyter.  If anyone has ideas on how to make it work, please let me know!
First, let's setup some common parameters.
// For random tree generation
let random = Random((int) DateTime.Now.Ticks &&& 0x0000FFFF)
let mutable randLock = Object()
/// Convenience method to generate random numbers safely.
let getNextRandLim (min, max) = lock randLock (fun () -> random.Next (min, max))
let getNextRand () = lock randLock (fun () -> random.Next ())
module Test =
    // Common parameters for the tests
    let numTests = 150 // How many test to run to check results
    let minDepth = 5 // Some complexity
    let maxDepth = 10 // To keep runtimes reasonable
    let percentNones = 10 // 10% of the time (1 out of 10), we can get a None instead of a Some
                          // Allows for unbalanced trees
    let minValueLength = 10 // Minimum length of ``val``
    let maxValueLength = 20 // Maximum length of ``val``
    let rootValue = "root" // ``val`` for root node.
    let nameGen() : string = // Generates a ``val`` for the node
        let len = getNextRandLim(minValueLength, maxValueLength)
        seq {
            for i in 1 .. len do
                let isCapital = getNextRand() % 2
                if isCapital = 0 then yield (getNextRandLim(65, 90) |> char |> string)
                else yield (getNextRandLim(97, 122) |> char |> string)
        }
        |> Seq.fold (fun acc e -> acc + e) ""
    // Generate test cases, limiting depth and complexity
    let CustomTreeGenerator (ctor:string * 'a option * 'a option -> 'a option) : 'a =
        let rec helper depth =
            match depth with
            | 0 -> None
            | n when n>0 ->
                let choice = getNextRand() % percentNones // None or Some
                match choice with
                | 0 -> None
                | _ -> ctor(nameGen(), helper <| depth - 1, helper <| depth - 1)
            | _ -> invalidArg "depth" (sprintf "Depth is %d but must be >= 0" depth)
        ctor(rootValue,
            helper <| getNextRandLim(minDepth, maxDepth),
            helper <| getNextRandLim(minDepth, maxDepth))
        |> Option.get
module EqualityTest =
    open Expecto
    type DU = Model.DUNode
    type R = Model.RNode
    type C = Model.CNode
    let tests =
        testList "Equality tests" <|
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm a DUNode tree is equal to itself %i" i) {
                    let tree = Test.CustomTreeGenerator DU.createOpt
                    Expect.equal tree tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm a RNode tree is equal to itself %i" i) {
                    let tree = Test.CustomTreeGenerator R.createOpt
                    Expect.equal tree tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm a CNode tree is equal to itself %i" i) {
                    let tree = Test.CustomTreeGenerator C.createOpt
                    Expect.equal tree tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm 2 different DUNode trees are not equal %i" i) {
                    let tree =
                        Test.CustomTreeGenerator DU.createOpt
                        |> fun (DU.Node(v, l, r)) -> DU.create("1", l, r)
                    let tree2 =
                        Test.CustomTreeGenerator DU.createOpt
                        |> fun (DU.Node(v, l, r)) -> DU.create("2", l, r)
                    Expect.notEqual tree tree2 (sprintf "%O %O" tree tree2)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm 2 different RNode trees are not equal %i" i) {
                    let tree =
                        Test.CustomTreeGenerator R.createOpt
                        |> fun t -> { t with ``val`` = "1" }
                    let tree2 =
                        Test.CustomTreeGenerator R.createOpt
                        |> fun t -> { t with ``val`` = "2" }
                    Expect.notEqual tree tree2 (sprintf "%O %O" tree tree2)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm 2 different CNode trees are not equal %i" i) {
                    let tree =
                        Test.CustomTreeGenerator C.createOpt
                        |> fun t -> C.create("1", t.left, t.right)
                    let tree2 =
                        Test.CustomTreeGenerator C.createOpt
                        |> fun t -> C.create("2", t.left, t.right)
                    Expect.notEqual tree tree2 (sprintf "%O %O" tree tree2)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm 2 similar DUNode trees are not equal %i" i) {
                    let tr = Test.CustomTreeGenerator DU.createOpt
                    let tree = tr |> fun (DU.Node(v, l, r)) -> DU.create("1", l, r)
                    let tree2 = tr |> fun (DU.Node(v, l, r)) -> DU.create("2", l, r)
                    Expect.notEqual tree tree2 (sprintf "%O %O" tree tree2)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm 2 similar RNode trees are not equal %i" i) {
                    let tr = Test.CustomTreeGenerator R.createOpt
                    let tree = tr |> fun t -> { t with ``val`` = "1" }
                    let tree2 = tr |> fun t -> { t with ``val`` = "2" }
                    Expect.notEqual tree tree2 (sprintf "%O %O" tree tree2)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "Confirm 2 similar CNode trees are not equal %i" i) {
                    let tr = Test.CustomTreeGenerator C.createOpt
                    let tree = tr |> fun t -> C.create("1", t.left, t.right)
                    let tree2 = tr |> fun t -> C.create("2", t.left, t.right)
                    Expect.notEqual tree tree2 (sprintf "%O %O" tree tree2)
                }
            ]
    runTests { defaultConfig with ``parallel`` = true } tests
module AutoSerializerTest =
    open Expecto
    open AutoSerializer
    type DU = Model.DUNode
    type R = Model.RNode
    type C = Model.CNode
    /// Basic brute-force, randomized tests to ensure the logic works correctly
    let tests =
        testList "Auto Serializer tests" <|
            test "DU provided tests" {
                let tree =
                    DU.create("root", DU.createOpt("left", DU.createOpt("left.left")), DU.createOpt("right"))
                Expect.equal (tree |> serializeDUNode |> deserializeDUNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            test "R provided tests" {
                let tree =
                    R.create("root", R.createOpt("left", R.createOpt("left.left")), R.createOpt("right"))
                Expect.equal (tree |> serializeRNode |> deserializeRNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            test "C provided tests" {
                let tree =
                    C.create("root", C.createOpt("left", C.createOpt("left.left")), C.createOpt("right"))
                Expect.equal (tree |> serializeCNode |> deserializeCNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "DUNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator DU.createOpt
                    Expect.equal (tree |> serializeDUNode |> deserializeDUNode) tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "RNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator R.createOpt
                    Expect.equal (tree |> serializeRNode |> deserializeRNode) tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "CNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator C.createOpt
                    Expect.equal (tree |> serializeCNode |> deserializeCNode) tree (sprintf "%O" tree)
                }
            ]
    runTests { defaultConfig with ``parallel`` = true } tests
module ManualSerializerTest =
    open Expecto
    open ManualSerializer
    type DU = Model.DUNode
    type R = Model.RNode
    type C = Model.CNode
    /// Basic brute-force, randomized tests to ensure the logic works correctly
    let tests =
        testList "Auto Serializer tests" <|
            test "DU provided tests" {
                let tree =
                    DU.create("root", DU.createOpt("left", DU.createOpt("left.left")), DU.createOpt("right"))
                Expect.equal (tree |> serializeDUNode |> deserializeDUNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            test "R provided tests" {
                let tree =
                    R.create("root", R.createOpt("left", R.createOpt("left.left")), R.createOpt("right"))
                Expect.equal (tree |> serializeRNode |> deserializeRNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            test "C provided tests" {
                let tree =
                    C.create("root", C.createOpt("left", C.createOpt("left.left")), C.createOpt("right"))
                Expect.equal (tree |> serializeCNode |> deserializeCNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "DUNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator DU.createOpt
                    Expect.equal (tree |> serializeDUNode |> deserializeDUNode) tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "RNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator R.createOpt
                    Expect.equal (tree |> serializeRNode |> deserializeRNode) tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "CNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator C.createOpt
                    Expect.equal (tree |> serializeCNode |> deserializeCNode) tree (sprintf "%O" tree)
                }
            ]
    runTests { defaultConfig with ``parallel`` = true } tests
module CustomSerializerTest =
    open Expecto
    open CustomSerializer
    type DU = Model.DUNode
    type R = Model.RNode
    type C = Model.CNode
    /// Basic brute-force, randomized tests to ensure the logic works correctly
    let tests =
        testList "Custom Serializer tests" <|
            test "DU provided tests" {
                let tree =
                    DU.create("root", DU.createOpt("left", DU.createOpt("left.left")), DU.createOpt("right"))
                Expect.equal (tree |> serializeDUNode |> deserializeDUNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            test "R provided tests" {
                let tree =
                    R.create("root", R.createOpt("left", R.createOpt("left.left")), R.createOpt("right"))
                Expect.equal (tree |> serializeRNode |> deserializeRNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            test "C provided tests" {
                let tree =
                    C.create("root", C.createOpt("left", C.createOpt("left.left")), C.createOpt("right"))
                Expect.equal (tree |> serializeCNode |> deserializeCNode) tree (sprintf "%O" tree)
                Expect.equal "left.left" tree.left.Value.left.Value.``val`` "tree's left.left = left.left"
            } ::
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "DUNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator DU.createOpt
                    Expect.equal (tree |> serializeDUNode |> deserializeDUNode) tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "RNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator R.createOpt
                    Expect.equal (tree |> serializeRNode |> deserializeRNode) tree (sprintf "%O" tree)
                }
            ] @
            [ for i in 1 .. Test.numTests do
                yield test (sprintf "CNode generated tests %i" i) {
                    let tree = Test.CustomTreeGenerator C.createOpt
                    Expect.equal (tree |> serializeCNode |> deserializeCNode) tree (sprintf "%O" tree)
                }
            ]
    runTests { defaultConfig with ``parallel`` = true } tests
Excellent, our logic works!
Now that we have it working, let's try some basic performance tests.
Performance Testing
Here is the strategy for performance testing.
Using random, pre-generated trees, test the following:
- Serialize trees (e.g. 1,000 of each model) using each of the 3 serialization methods.
 - De-serialize those trees using each of the 3 deserialization methods.
 
Hopefully, this will give us an indication of which combination of de-/serialization method and data structure is the "fastest".
Setup the test data
The first step is to create the trees that will be used for serialization and de-serialization.
module PerformanceTestData =
    type DU = Model.DUNode
    type R = Model.RNode
    type C = Model.CNode
    // How many trees to generate
    let dataStructureCount = 2000
    // Generate the trees
    let DUTrees = List.init dataStructureCount (fun _ -> Test.CustomTreeGenerator DU.createOpt)
    let RTrees = List.init dataStructureCount (fun _ -> Test.CustomTreeGenerator R.createOpt)
    let CTrees = List.init dataStructureCount (fun _ -> Test.CustomTreeGenerator C.createOpt)
    // To measure each execution
    let stopWatch = System.Diagnostics.Stopwatch()
    // The rows and columns of the result table.
    let rows = [
        "Automatic"
        "Manual"
        "Custom"
    ]
    let columns = ["Discriminated Union"; "Record"; "Class"]
    let cornerText = [@"Algorithm vs. Data Structure"];
    // The algorithms, for "somewhat easy" testing
    let duSerializationAlgorithms =
        [ AutoSerializer.serializeDUNode
          ManualSerializer.serializeDUNode
          CustomSerializer.serializeDUNode ]
    let rSerializationAlgorithms =
        [ AutoSerializer.serializeRNode
          ManualSerializer.serializeRNode
          CustomSerializer.serializeRNode ]
    let cSerializationAlgorithms =
        [ AutoSerializer.serializeCNode
          ManualSerializer.serializeCNode
          CustomSerializer.serializeCNode ]
    let duDeserializationAlgorithms =
        [ AutoSerializer.deserializeDUNode
          ManualSerializer.deserializeDUNode
          CustomSerializer.deserializeDUNode ]
    let rDeserializationAlgorithms =
        [ AutoSerializer.deserializeRNode
          ManualSerializer.deserializeRNode
          CustomSerializer.deserializeRNode ]
    let cDeserializationAlgorithms =
        [ AutoSerializer.deserializeCNode
          ManualSerializer.deserializeCNode
          CustomSerializer.deserializeCNode ]
open PerformanceTestData
/// Returns an average measurement of 3 consecutive runs.
let measurement numRuns alg lst =
    stopWatch.Reset()
    stopWatch.Start()
    List.init numRuns (fun i -> List.map alg lst)
    |> ignore
    stopWatch.Stop()
    TimeSpan.FromTicks(stopWatch.ElapsedTicks / (int64 numRuns)).Ticks
/// Collect the serialization results.
let serializationResults =
    [ yield
        duSerializationAlgorithms
        |> List.map (fun a -> measurement 3 a DUTrees)
        |> List.zip rows ] @
    [ yield
        rSerializationAlgorithms
        |> List.map (fun a -> measurement 3 a RTrees)
        |> List.zip rows ] @
    [ yield
        cSerializationAlgorithms
        |> List.map (fun a -> measurement 3 a CTrees)
        |> List.zip rows ]
/// Collect the deserialization results.
let deserializationResults =
    [ let ser = List.map (fun a -> List.map a DUTrees) duSerializationAlgorithms
      yield
        duDeserializationAlgorithms
        |> List.mapi (fun i a -> measurement 3 a (List.item i ser))
        |> List.zip rows ] @
    [ let ser = List.map (fun a -> List.map a RTrees) rSerializationAlgorithms
      yield
        rDeserializationAlgorithms
        |> List.mapi (fun i a -> measurement 3 a (List.item i ser))
        |> List.zip rows ] @
    [ let ser = List.map (fun a -> List.map a CTrees) cSerializationAlgorithms
      yield
        cDeserializationAlgorithms
        |> List.mapi (fun i a -> measurement 3 a (List.item i ser))
        |> List.zip rows ]
open XPlot.GoogleCharts
serializationResults
|> List.map (fun l -> List.map (fun (x, y) -> (x, TimeSpan.FromTicks(y).ToString("c"))) l)
|> Chart.Table
|> Chart.WithOptions (Options(minColor="green", midColor="yellow", maxColor="red", showRowNumber=true))
|> Chart.WithLabels (cornerText @ columns)
And as a chart, showing ticks.
serializationResults
|> Chart.Bar
|> Chart.WithOptions (Options())
|> Chart.WithLabels (cornerText @ columns)
deserializationResults
|> List.map (fun l -> List.map (fun (x, y) -> (x, TimeSpan.FromTicks(y).ToString("c"))) l)
|> Chart.Table
|> Chart.WithOptions (Options(minColor="green", midColor="yellow", maxColor="red", showRowNumber=true))
|> Chart.WithLabels (cornerText @ columns)
And as a chart, showing ticks.
deserializationResults
|> Chart.Bar
|> Chart.WithOptions (Options())
|> Chart.WithLabels (cornerText @ columns)
Conclusion
The results are very interesting. From a CPU perspective, FParsec wins hands down regardless of the chosen data structure. There is an order of magnitude difference between the three serialization techniques.
From an SLOC perspective, Json.NET is the clear winner. Serialization and deserialization are both 1 line, each. FParsec is the most complex, requiring the development of a custom de-/serialization format, then ensuring that all 3 data structures can be transformed to that format.
Chiron stays right in the middle, both in terms of performance and SLOC.
The one advantage that both Chiron and FParsec can provide is that, when created the right way, the serialized string can be deserialized into any of the 3 data structures (discriminated union, record, or class). The FParsec implementation above works that way.
Unfortunately, I do not know how to measure memory usage from within IfSharp / Jupyter. So, I can't really speak to that.
From a data structure perspective, records seems to provide very good performance. I was surprised that records performed even better than classes, in general. However, it appears that Json.NET had issues with both discriminated unions and classes, during deserialization and serialization, respectively.
Personally, I am not sure that creating a custom de-/serialization format (aka, going down the FParsec route) is worth the effort, especially given all the edge cases that must be handled. For example, my FParsec implementation cannot handle double-quotes within strings and, I believe (though I didn't test this), cannot handle strings with newline characters in them.
Json.NET and Chiron both use JSON, which is an extremely popular format and enjoys widespread support in development tooling. I believe this is a better route, especially because using such a common, human-readable format would make debugging easier. Maintainability is extremely important and something that is often overlooked.
After testing the system with JSON, it can be shifted to a binary format such as MessagePack, BSON, or Protocol Buffers, among others, if size becomes a concern. Most of these formats have well-tested libraries available in multiple languages.