layout: post
title: “Trees in the real world”
description: “Examples using databases, JSON and error handling”
seriesId: “Recursive types and folds”
seriesOrder: 6

categories: [Folds, Patterns]

This post is the sixth in a series.

In the previous post, we briefly looked at some generic types.

In this post, we’ll do some deeper dives into some real-world examples of using trees and folds.

Series contents

Here’s the contents of this series:


Defining a generic Tree type

In this post, we’ll be working with a generic Tree inspired by the FileSystem domain that we explored earlier.

Here was the original design:

  1. type FileSystemItem =
  2. | File of FileInfo
  3. | Directory of DirectoryInfo
  4. and FileInfo = {name:string; fileSize:int}
  5. and DirectoryInfo = {name:string; dirSize:int; subitems:FileSystemItem list}

We can separate out the data from the recursion, and create a generic Tree type like this:

  1. type Tree<'LeafData,'INodeData> =
  2. | LeafNode of 'LeafData
  3. | InternalNode of 'INodeData * Tree<'LeafData,'INodeData> seq

Notice that I have used seq to represent the subitems rather than list. The reason for this will become apparent shortly.

The file system domain can then be modelled using Tree by specifying FileInfo as data associated with a leaf node and DirectoryInfo as data associated with an internal node:

  1. type FileInfo = {name:string; fileSize:int}
  2. type DirectoryInfo = {name:string; dirSize:int}
  3. type FileSystemItem = Tree<FileInfo,DirectoryInfo>

cata and fold for Tree

We can define cata and fold in the usual way:

  1. module Tree =
  2. let rec cata fLeaf fNode (tree:Tree<'LeafData,'INodeData>) :'r =
  3. let recurse = cata fLeaf fNode
  4. match tree with
  5. | LeafNode leafInfo ->
  6. fLeaf leafInfo
  7. | InternalNode (nodeInfo,subtrees) ->
  8. fNode nodeInfo (subtrees |> Seq.map recurse)
  9. let rec fold fLeaf fNode acc (tree:Tree<'LeafData,'INodeData>) :'r =
  10. let recurse = fold fLeaf fNode
  11. match tree with
  12. | LeafNode leafInfo ->
  13. fLeaf acc leafInfo
  14. | InternalNode (nodeInfo,subtrees) ->
  15. // determine the local accumulator at this level
  16. let localAccum = fNode acc nodeInfo
  17. // thread the local accumulator through all the subitems using Seq.fold
  18. let finalAccum = subtrees |> Seq.fold recurse localAccum
  19. // ... and return it
  20. finalAccum

Note that I am not going to implement foldBack for the Tree type, because it’s unlikely that the tree will get so deep as to cause a stack overflow.
Functions that need inner data can use cata.

Modelling the File System domain with Tree

Let’s test it with the same values that we used before:

  1. let fromFile (fileInfo:FileInfo) =
  2. LeafNode fileInfo
  3. let fromDir (dirInfo:DirectoryInfo) subitems =
  4. InternalNode (dirInfo,subitems)
  5. let readme = fromFile {name="readme.txt"; fileSize=1}
  6. let config = fromFile {name="config.xml"; fileSize=2}
  7. let build = fromFile {name="build.bat"; fileSize=3}
  8. let src = fromDir {name="src"; dirSize=10} [readme; config; build]
  9. let bin = fromDir {name="bin"; dirSize=10} []
  10. let root = fromDir {name="root"; dirSize=5} [src; bin]

The totalSize function is almost identical to the one in the previous post:

  1. let totalSize fileSystemItem =
  2. let fFile acc (file:FileInfo) =
  3. acc + file.fileSize
  4. let fDir acc (dir:DirectoryInfo)=
  5. acc + dir.dirSize
  6. Tree.fold fFile fDir 0 fileSystemItem
  7. readme |> totalSize // 1
  8. src |> totalSize // 16 = 10 + (1 + 2 + 3)
  9. root |> totalSize // 31 = 5 + 16 + 10

And so is the largestFile function:

  1. let largestFile fileSystemItem =
  2. let fFile (largestSoFarOpt:FileInfo option) (file:FileInfo) =
  3. match largestSoFarOpt with
  4. | None ->
  5. Some file
  6. | Some largestSoFar ->
  7. if largestSoFar.fileSize > file.fileSize then
  8. Some largestSoFar
  9. else
  10. Some file
  11. let fDir largestSoFarOpt dirInfo =
  12. largestSoFarOpt
  13. // call the fold
  14. Tree.fold fFile fDir None fileSystemItem
  15. readme |> largestFile
  16. // Some {name = "readme.txt"; fileSize = 1}
  17. src |> largestFile
  18. // Some {name = "build.bat"; fileSize = 3}
  19. bin |> largestFile
  20. // None
  21. root |> largestFile
  22. // Some {name = "build.bat"; fileSize = 3}

The source code for this section is available at this gist.

The Tree type in the real world

We can use the Tree to model the real file system too! To do this,
just set the leaf node type to System.IO.FileInfo and the internal node type to System.IO.DirectoryInfo.

  1. open System
  2. open System.IO
  3. type FileSystemTree = Tree<IO.FileInfo,IO.DirectoryInfo>

And let’s create some helper methods to create the various nodes:

  1. let fromFile (fileInfo:FileInfo) =
  2. LeafNode fileInfo
  3. let rec fromDir (dirInfo:DirectoryInfo) =
  4. let subItems = seq{
  5. yield! dirInfo.EnumerateFiles() |> Seq.map fromFile
  6. yield! dirInfo.EnumerateDirectories() |> Seq.map fromDir
  7. }
  8. InternalNode (dirInfo,subItems)

Now you can see why I used seq rather than list for the subitems. The seq is lazy, which means that we can create nodes
without actually hitting the disk.

Here’s the totalSize function again, this time using the real file information:

  1. let totalSize fileSystemItem =
  2. let fFile acc (file:FileInfo) =
  3. acc + file.Length
  4. let fDir acc (dir:DirectoryInfo)=
  5. acc
  6. Tree.fold fFile fDir 0L fileSystemItem

Let’s see what the size of the current directory is:

  1. // set the current directory to the current source directory
  2. Directory.SetCurrentDirectory __SOURCE_DIRECTORY__
  3. // get the current directory as a Tree
  4. let currentDir = fromDir (DirectoryInfo("."))
  5. // get the size of the current directory
  6. currentDir |> totalSize

Similarly, we can get the largest file:

  1. let largestFile fileSystemItem =
  2. let fFile (largestSoFarOpt:FileInfo option) (file:FileInfo) =
  3. match largestSoFarOpt with
  4. | None ->
  5. Some file
  6. | Some largestSoFar ->
  7. if largestSoFar.Length > file.Length then
  8. Some largestSoFar
  9. else
  10. Some file
  11. let fDir largestSoFarOpt dirInfo =
  12. largestSoFarOpt
  13. // call the fold
  14. Tree.fold fFile fDir None fileSystemItem
  15. currentDir |> largestFile

So that’s one big benefit of using generic recursive types. If we can turn a real-world hierarchy into our tree structure, we can get all the benefits of fold “for free”.

Mapping with generic types

One other advantage of using generic types is that you can do things like map — converting every element to a new type without changing the structure.

We can see this in action with the real world file system. But first we need to define map for the Tree type!

The implementation of map can also be done mechanically, using the following rules:

  • Create a function parameter to handle each case in the structure.
  • For non-recursive cases
    • First, use the function parameter to transform the non-recursive data associated with that case
    • Then wrap the result in the same case constructor
  • For recursive cases, perform two steps:
    • First, use the function parameter to transform the non-recursive data associated with that case
    • Next, recursively map the nested values.
    • Finally, wrap the results in the same case constructor

Here’s the implementation of map for Tree, created by following those rules:

  1. module Tree =
  2. let rec cata ...
  3. let rec fold ...
  4. let rec map fLeaf fNode (tree:Tree<'LeafData,'INodeData>) =
  5. let recurse = map fLeaf fNode
  6. match tree with
  7. | LeafNode leafInfo ->
  8. let newLeafInfo = fLeaf leafInfo
  9. LeafNode newLeafInfo
  10. | InternalNode (nodeInfo,subtrees) ->
  11. let newNodeInfo = fNode nodeInfo
  12. let newSubtrees = subtrees |> Seq.map recurse
  13. InternalNode (newNodeInfo, newSubtrees)

If we look at the signature of Tree.map, we can see that all the leaf data is transformed to type 'a, all the node data is transformed to type 'b,
and the final result is a Tree<'a,'b>.

  1. val map :
  2. fLeaf:('LeafData -> 'a) ->
  3. fNode:('INodeData -> 'b) ->
  4. tree:Tree<'LeafData,'INodeData> ->
  5. Tree<'a,'b>

We can define Tree.iter in a similar way:

  1. module Tree =
  2. let rec map ...
  3. let rec iter fLeaf fNode (tree:Tree<'LeafData,'INodeData>) =
  4. let recurse = iter fLeaf fNode
  5. match tree with
  6. | LeafNode leafInfo ->
  7. fLeaf leafInfo
  8. | InternalNode (nodeInfo,subtrees) ->
  9. subtrees |> Seq.iter recurse
  10. fNode nodeInfo


Example: Creating a directory listing

Let’s say we want to use map to transform the file system into a directory listing - a tree of strings where each string has information
about the corresponding file or directory. Here’s how we could do it:

  1. let dirListing fileSystemItem =
  2. let printDate (d:DateTime) = d.ToString()
  3. let mapFile (fi:FileInfo) =
  4. sprintf "%10i %s %-s" fi.Length (printDate fi.LastWriteTime) fi.Name
  5. let mapDir (di:DirectoryInfo) =
  6. di.FullName
  7. Tree.map mapFile mapDir fileSystemItem

And then we can print the strings out like this:

  1. currentDir
  2. |> dirListing
  3. |> Tree.iter (printfn "%s") (printfn "\n%s")

The results will look something like this:

  1. 8315 10/08/2015 23:37:41 Fold.fsx
  2. 3680 11/08/2015 23:59:01 FoldAndRecursiveTypes.fsproj
  3. 1010 11/08/2015 01:19:07 FoldAndRecursiveTypes.sln
  4. 1107 11/08/2015 23:59:01 HtmlDom.fsx
  5. 79 11/08/2015 01:21:54 LinkedList.fsx

The source code for this example is available at this gist.


Example: Creating a parallel grep

Let’s look at a more complex example. I’ll demonstrate how to create a parallel “grep” style search using fold.

The logic will be like this:

  • Use fold to iterate through the files.
  • For each file, if its name doesn’t match the desired file pattern, return None.
  • If the file is to be processed, then return an async that returns all the line matches in the file.
  • Next, all these asyncs — the output of the fold — are aggregated into a sequence.
  • The sequence of asyncs is transformed into a single one using Async.Parallel which returns a list of results.

Before we start writing the main code, we’ll need some helper functions.

First, a generic function that folds over the lines in a file asynchronously.
This will be the basis of the pattern matching.

  1. /// Fold over the lines in a file asynchronously
  2. /// passing in the current line and line number tothe folder function.
  3. ///
  4. /// Signature:
  5. /// folder:('a -> int -> string -> 'a) ->
  6. /// acc:'a ->
  7. /// fi:FileInfo ->
  8. /// Async<'a>
  9. let foldLinesAsync folder acc (fi:FileInfo) =
  10. async {
  11. let mutable acc = acc
  12. let mutable lineNo = 1
  13. use sr = new StreamReader(path=fi.FullName)
  14. while not sr.EndOfStream do
  15. let! lineText = sr.ReadLineAsync() |> Async.AwaitTask
  16. acc <- folder acc lineNo lineText
  17. lineNo <- lineNo + 1
  18. return acc
  19. }

Next, a little helper that allows us to map over Async values:

  1. let asyncMap f asyncX = async {
  2. let! x = asyncX
  3. return (f x) }

Now for the central logic. We will create a function that, given a textPattern and a FileInfo, will return a list of lines that match the textPattern, but asynchronously:

  1. /// return the matching lines in a file, as an async<string list>
  2. let matchPattern textPattern (fi:FileInfo) =
  3. // set up the regex
  4. let regex = Text.RegularExpressions.Regex(pattern=textPattern)
  5. // set up the function to use with "fold"
  6. let folder results lineNo lineText =
  7. if regex.IsMatch lineText then
  8. let result = sprintf "%40s:%-5i %s" fi.Name lineNo lineText
  9. result :: results
  10. else
  11. // pass through
  12. results
  13. // main flow
  14. fi
  15. |> foldLinesAsync folder []
  16. // the fold output is in reverse order, so reverse it
  17. |> asyncMap List.rev

And now for the grep function itself:

  1. let grep filePattern textPattern fileSystemItem =
  2. let regex = Text.RegularExpressions.Regex(pattern=filePattern)
  3. /// if the file matches the pattern
  4. /// do the matching and return Some async, else None
  5. let matchFile (fi:FileInfo) =
  6. if regex.IsMatch fi.Name then
  7. Some (matchPattern textPattern fi)
  8. else
  9. None
  10. /// process a file by adding its async to the list
  11. let fFile asyncs (fi:FileInfo) =
  12. // add to the list of asyncs
  13. (matchFile fi) :: asyncs
  14. // for directories, just pass through the list of asyncs
  15. let fDir asyncs (di:DirectoryInfo) =
  16. asyncs
  17. fileSystemItem
  18. |> Tree.fold fFile fDir [] // get the list of asyncs
  19. |> Seq.choose id // choose the Somes (where a file was processed)
  20. |> Async.Parallel // merge all asyncs into a single async
  21. |> asyncMap (Array.toList >> List.collect id) // flatten array of lists into a single list

Let’s test it!

  1. currentDir
  2. |> grep "fsx" "LinkedList"
  3. |> Async.RunSynchronously

The result will look something like this:

  1. " SizeOfTypes.fsx:120 type LinkedList<'a> = ";
  2. " SizeOfTypes.fsx:122 | Cell of head:'a * tail:LinkedList<'a>";
  3. " SizeOfTypes.fsx:125 let S = size(LinkedList<'a>)";
  4. " RecursiveTypesAndFold-3.fsx:15 // LinkedList";
  5. " RecursiveTypesAndFold-3.fsx:18 type LinkedList<'a> = ";
  6. " RecursiveTypesAndFold-3.fsx:20 | Cons of head:'a * tail:LinkedList<'a>";
  7. " RecursiveTypesAndFold-3.fsx:26 module LinkedList = ";
  8. " RecursiveTypesAndFold-3.fsx:39 list:LinkedList<'a> ";
  9. " RecursiveTypesAndFold-3.fsx:64 list:LinkedList<'a> -> ";

That’s not bad for about 40 lines of code. This conciseness is because we are using various kinds of fold and map which hide the recursion, allowing
us to focus on the pattern matching logic itself.

Of course, this is not at all efficient or optimized (an async for every line!), and so I wouldn’t use it as a real implementation, but it does give you an idea of the power of fold.

The source code for this example is available at this gist.


Example: Storing the file system in a database

For the next example, let’s look at how to store a file system tree in a database. I don’t really know why you would want to do that, but
the principles would work equally well for storing any hierarchical structure, so I will demonstrate it anyway!

To model the file system hierarchy in the database, say that we have four tables:

  • DbDir stores information about each directory.
  • DbFile stores information about each file.
  • DbDir_File stores the relationship between a directory and a file.
  • DbDir_Dir stores the relationship between a parent directory and a child directory.

Here are the database table definitions:

  1. CREATE TABLE DbDir (
  2. DirId int IDENTITY NOT NULL,
  3. Name nvarchar(50) NOT NULL
  4. )
  5. CREATE TABLE DbFile (
  6. FileId int IDENTITY NOT NULL,
  7. Name nvarchar(50) NOT NULL,
  8. FileSize int NOT NULL
  9. )
  10. CREATE TABLE DbDir_File (
  11. DirId int NOT NULL,
  12. FileId int NOT NULL
  13. )
  14. CREATE TABLE DbDir_Dir (
  15. ParentDirId int NOT NULL,
  16. ChildDirId int NOT NULL
  17. )

That’s simple enough. But note that in order to save a directory completely along with its relationships to its child items, we first need the ids of all its children,
and each child directory needs the ids of its children, and so on.

This implies that we should use cata instead of fold, so that we have access to the data from the lower levels of the hierarchy.

Implementing the database functions

We’re not wise enough to be using the SQL Provider and so we have written our
own table insertion functions, like this dummy one:

  1. /// Insert a DbFile record
  2. let insertDbFile name (fileSize:int64) =
  3. let id = nextIdentity()
  4. printfn "%10s: inserting id:%i name:%s size:%i" "DbFile" id name fileSize

In a real database, the identity column would be automatically generated for you, but for this example, I’ll use a little helper function nextIdentity:

  1. let nextIdentity =
  2. let id = ref 0
  3. fun () ->
  4. id := !id + 1
  5. !id
  6. // test
  7. nextIdentity() // 1
  8. nextIdentity() // 2
  9. nextIdentity() // 3

Now in order to insert a directory, we need to first know all the ids of the files in the directory. This implies that the insertDbFile function should
return the id that was generated.

  1. /// Insert a DbFile record and return the new file id
  2. let insertDbFile name (fileSize:int64) =
  3. let id = nextIdentity()
  4. printfn "%10s: inserting id:%i name:%s size:%i" "DbFile" id name fileSize
  5. id

But that logic applies to the directories too:

  1. /// Insert a DbDir record and return the new directory id
  2. let insertDbDir name =
  3. let id = nextIdentity()
  4. printfn "%10s: inserting id:%i name:%s" "DbDir" id name
  5. id

But that’s still not good enough. When the child ids are passed to the parent directory, it needs to distinguish between files and directories, because
the relations are stored in different tables.

No problem — we’ll just use a choice type to distinguish between them!

  1. type PrimaryKey =
  2. | FileId of int
  3. | DirId of int

With this in place, we can complete the implementation of the database functions:

  1. /// Insert a DbFile record and return the new PrimaryKey
  2. let insertDbFile name (fileSize:int64) =
  3. let id = nextIdentity()
  4. printfn "%10s: inserting id:%i name:%s size:%i" "DbFile" id name fileSize
  5. FileId id
  6. /// Insert a DbDir record and return the new PrimaryKey
  7. let insertDbDir name =
  8. let id = nextIdentity()
  9. printfn "%10s: inserting id:%i name:%s" "DbDir" id name
  10. DirId id
  11. /// Insert a DbDir_File record
  12. let insertDbDir_File dirId fileId =
  13. printfn "%10s: inserting parentDir:%i childFile:%i" "DbDir_File" dirId fileId
  14. /// Insert a DbDir_Dir record
  15. let insertDbDir_Dir parentDirId childDirId =
  16. printfn "%10s: inserting parentDir:%i childDir:%i" "DbDir_Dir" parentDirId childDirId

Working with the catamorphism

As noted above, we need to use cata instead of fold, because we need the inner ids at each step.

The function to handle the File case is easy — just insert it and return the PrimaryKey.

  1. let fFile (fi:FileInfo) =
  2. insertDbFile fi.Name fi.Length

The function to handle the Directory case will be passed the DirectoryInfo and a sequence of PrimaryKeys from the children that have already been inserted.

It should insert the main directory record, then insert the children, and then return the PrimaryKey for the next higher level:

  1. let fDir (di:DirectoryInfo) childIds =
  2. let dirId = insertDbDir di.Name
  3. // insert the children
  4. // return the id to the parent
  5. dirId

After inserting the directory record and getting its id, for each child id, we insert either into the DbDir_File table or the DbDir_Dir,
depending on the type of the childId.

  1. let fDir (di:DirectoryInfo) childIds =
  2. let dirId = insertDbDir di.Name
  3. let parentPK = pkToInt dirId
  4. childIds |> Seq.iter (fun childId ->
  5. match childId with
  6. | FileId fileId -> insertDbDir_File parentPK fileId
  7. | DirId childDirId -> insertDbDir_Dir parentPK childDirId
  8. )
  9. // return the id to the parent
  10. dirId

Note that I’ve also created a little helper function pkToInt that extracts the integer id from the PrimaryKey type.

Here is all the code in one chunk:

  1. open System
  2. open System.IO
  3. let nextIdentity =
  4. let id = ref 0
  5. fun () ->
  6. id := !id + 1
  7. !id
  8. type PrimaryKey =
  9. | FileId of int
  10. | DirId of int
  11. /// Insert a DbFile record and return the new PrimaryKey
  12. let insertDbFile name (fileSize:int64) =
  13. let id = nextIdentity()
  14. printfn "%10s: inserting id:%i name:%s size:%i" "DbFile" id name fileSize
  15. FileId id
  16. /// Insert a DbDir record and return the new PrimaryKey
  17. let insertDbDir name =
  18. let id = nextIdentity()
  19. printfn "%10s: inserting id:%i name:%s" "DbDir" id name
  20. DirId id
  21. /// Insert a DbDir_File record
  22. let insertDbDir_File dirId fileId =
  23. printfn "%10s: inserting parentDir:%i childFile:%i" "DbDir_File" dirId fileId
  24. /// Insert a DbDir_Dir record
  25. let insertDbDir_Dir parentDirId childDirId =
  26. printfn "%10s: inserting parentDir:%i childDir:%i" "DbDir_Dir" parentDirId childDirId
  27. let pkToInt primaryKey =
  28. match primaryKey with
  29. | FileId fileId -> fileId
  30. | DirId dirId -> dirId
  31. let insertFileSystemTree fileSystemItem =
  32. let fFile (fi:FileInfo) =
  33. insertDbFile fi.Name fi.Length
  34. let fDir (di:DirectoryInfo) childIds =
  35. let dirId = insertDbDir di.Name
  36. let parentPK = pkToInt dirId
  37. childIds |> Seq.iter (fun childId ->
  38. match childId with
  39. | FileId fileId -> insertDbDir_File parentPK fileId
  40. | DirId childDirId -> insertDbDir_Dir parentPK childDirId
  41. )
  42. // return the id to the parent
  43. dirId
  44. fileSystemItem
  45. |> Tree.cata fFile fDir

Now let’s test it:

  1. // get the current directory as a Tree
  2. let currentDir = fromDir (DirectoryInfo("."))
  3. // insert into the database
  4. currentDir
  5. |> insertFileSystemTree

The output should look something like this:

  1. DbDir: inserting id:41 name:FoldAndRecursiveTypes
  2. DbFile: inserting id:42 name:Fold.fsx size:8315
  3. DbDir_File: inserting parentDir:41 childFile:42
  4. DbFile: inserting id:43 name:FoldAndRecursiveTypes.fsproj size:3680
  5. DbDir_File: inserting parentDir:41 childFile:43
  6. DbFile: inserting id:44 name:FoldAndRecursiveTypes.sln size:1010
  7. DbDir_File: inserting parentDir:41 childFile:44
  8. ...
  9. DbDir: inserting id:57 name:bin
  10. DbDir: inserting id:58 name:Debug
  11. DbDir_Dir: inserting parentDir:57 childDir:58
  12. DbDir_Dir: inserting parentDir:41 childDir:57

You can see that the ids are being generated as the files are iterated over, and that each DbFile insert is followed by a DbDir_File insert.

The source code for this example is available at this gist.


Example: Serializing a Tree to JSON

Let’s look at another common challenge: serializing and deserializing a tree to JSON, XML, or some other format.

Let’s use the Gift domain again, but this time, we’ll model the Gift type as a tree. That means we get to put more than one thing in a box!

Modelling the Gift domain as a tree

Here are the main types again, but notice that the final Gift type is defined as a tree:

  1. type Book = {title: string; price: decimal}
  2. type ChocolateType = Dark | Milk | SeventyPercent
  3. type Chocolate = {chocType: ChocolateType ; price: decimal}
  4. type WrappingPaperStyle =
  5. | HappyBirthday
  6. | HappyHolidays
  7. | SolidColor
  8. // unified data for non-recursive cases
  9. type GiftContents =
  10. | Book of Book
  11. | Chocolate of Chocolate
  12. // unified data for recursive cases
  13. type GiftDecoration =
  14. | Wrapped of WrappingPaperStyle
  15. | Boxed
  16. | WithACard of string
  17. type Gift = Tree<GiftContents,GiftDecoration>

As usual, we can create some helper functions to assist with constructing a Gift:

  1. let fromBook book =
  2. LeafNode (Book book)
  3. let fromChoc choc =
  4. LeafNode (Chocolate choc)
  5. let wrapInPaper paperStyle innerGift =
  6. let container = Wrapped paperStyle
  7. InternalNode (container, [innerGift])
  8. let putInBox innerGift =
  9. let container = Boxed
  10. InternalNode (container, [innerGift])
  11. let withCard message innerGift =
  12. let container = WithACard message
  13. InternalNode (container, [innerGift])
  14. let putTwoThingsInBox innerGift innerGift2 =
  15. let container = Boxed
  16. InternalNode (container, [innerGift;innerGift2])

And we can create some sample data:

  1. let wolfHall = {title="Wolf Hall"; price=20m}
  2. let yummyChoc = {chocType=SeventyPercent; price=5m}
  3. let birthdayPresent =
  4. wolfHall
  5. |> fromBook
  6. |> wrapInPaper HappyBirthday
  7. |> withCard "Happy Birthday"
  8. let christmasPresent =
  9. yummyChoc
  10. |> fromChoc
  11. |> putInBox
  12. |> wrapInPaper HappyHolidays
  13. let twoBirthdayPresents =
  14. let thing1 = wolfHall |> fromBook
  15. let thing2 = yummyChoc |> fromChoc
  16. putTwoThingsInBox thing1 thing2
  17. |> wrapInPaper HappyBirthday
  18. let twoWrappedPresentsInBox =
  19. let thing1 = wolfHall |> fromBook |> wrapInPaper HappyHolidays
  20. let thing2 = yummyChoc |> fromChoc |> wrapInPaper HappyBirthday
  21. putTwoThingsInBox thing1 thing2

Functions like description now need to handle a list of inner texts, rather than one. We’ll just concat the strings together with an & separator:

  1. let description gift =
  2. let fLeaf leafData =
  3. match leafData with
  4. | Book book ->
  5. sprintf "'%s'" book.title
  6. | Chocolate choc ->
  7. sprintf "%A chocolate" choc.chocType
  8. let fNode nodeData innerTexts =
  9. let innerText = String.concat " & " innerTexts
  10. match nodeData with
  11. | Wrapped style ->
  12. sprintf "%s wrapped in %A paper" innerText style
  13. | Boxed ->
  14. sprintf "%s in a box" innerText
  15. | WithACard message ->
  16. sprintf "%s with a card saying '%s'" innerText message
  17. // main call
  18. Tree.cata fLeaf fNode gift

Finally, we can check that the function still works as before, and that multiple items are handled correctly:

  1. birthdayPresent |> description
  2. // "'Wolf Hall' wrapped in HappyBirthday paper with a card saying 'Happy Birthday'"
  3. christmasPresent |> description
  4. // "SeventyPercent chocolate in a box wrapped in HappyHolidays paper"
  5. twoBirthdayPresents |> description
  6. // "'Wolf Hall' & SeventyPercent chocolate in a box
  7. // wrapped in HappyBirthday paper"
  8. twoWrappedPresentsInBox |> description
  9. // "'Wolf Hall' wrapped in HappyHolidays paper
  10. // & SeventyPercent chocolate wrapped in HappyBirthday paper
  11. // in a box"

Step 1: Defining GiftDto

Our Gift type consists of many discriminated unions. In my experience, these do not serialize well. In fact, most complex types do not serialize well!

So what I like to do is define DTO types that are explicitly designed to be serialized well.
In practice this means that the DTO types are constrained as follows:

  • Only record types should be used.
  • The record fields should consist only primitive values such as int, string and bool.

By doing this, we also get some other advantages:

We gain control of the serialization output. These kinds of data types are handled the same by most serializers, while
“strange” things such as unions can be interpreted differently by different libraries.

We have better control of error handling. My number one rule when dealing with serialized data is “trust no one”.
It’s very common that the data is structured correctly but is invalid for the domain: supposedly non-null strings are null,
strings are too long, integers are outside the correct bounds, and so on.

By using DTOs, we can be sure that the deserialization step itself will work. Then, when we convert the DTO to a domain type, we can
do proper validation.

So, let’s define some DTO types for out domain. Each DTO type will correspond to a domain type, so let’s start with GiftContents.
We’ll define a corresponding DTO type called GiftContentsDto as follows:

  1. [<CLIMutableAttribute>]
  2. type GiftContentsDto = {
  3. discriminator : string // "Book" or "Chocolate"
  4. // for "Book" case only
  5. bookTitle: string
  6. // for "Chocolate" case only
  7. chocolateType : string // one of "Dark" "Milk" "SeventyPercent"
  8. // for all cases
  9. price: decimal
  10. }

Obviously, this quite different from the original GiftContents, so let’s look at the differences:

  • First, it has the CLIMutableAttribute, which allows deserializers to construct them using reflection.
  • Second, it has a discriminator which indicates which case of the original union type is being used. Obviously, this string could be set to anything,
    so when converting from the DTO back to the domain type, we’ll have to check that carefully!
  • Next is a series of fields, one for every possible item of data that needs to be stored. For example, in the Book case, we need a bookTitle,
    while in the Chocolate case, we need the chocolate type. And finally the price field which is in both types.
    Note that the chocolate type is stored as a string as well, and so will also need special treatment when we convert from DTO to domain.

The GiftDecorationDto type is created in the same way, with a discriminator and strings rather than unions.

  1. [<CLIMutableAttribute>]
  2. type GiftDecorationDto = {
  3. discriminator: string // "Wrapped" or "Boxed" or "WithACard"
  4. // for "Wrapped" case only
  5. wrappingPaperStyle: string // "HappyBirthday" or "HappyHolidays" or "SolidColor"
  6. // for "WithACard" case only
  7. message: string
  8. }

Finally, we can define a GiftDto type as being a tree that is composed of the two DTO types:

  1. type GiftDto = Tree<GiftContentsDto,GiftDecorationDto>

Step 2: Transforming a Gift to a GiftDto

Now that we have this DTO type, all we need to do is use Tree.map to convert from a Gift to a GiftDto.
And in order to do that, we need to create two functions: one that converts from GiftContents to GiftContentsDto and one
that converts from GiftDecoration to GiftDecorationDto.

Here’s the complete code for giftToDto, which should be self-explanatory:

  1. let giftToDto (gift:Gift) :GiftDto =
  2. let fLeaf leafData :GiftContentsDto =
  3. match leafData with
  4. | Book book ->
  5. {discriminator= "Book"; bookTitle=book.title; chocolateType=null; price=book.price}
  6. | Chocolate choc ->
  7. let chocolateType = sprintf "%A" choc.chocType
  8. {discriminator= "Chocolate"; bookTitle=null; chocolateType=chocolateType; price=choc.price}
  9. let fNode nodeData :GiftDecorationDto =
  10. match nodeData with
  11. | Wrapped style ->
  12. let wrappingPaperStyle = sprintf "%A" style
  13. {discriminator= "Wrapped"; wrappingPaperStyle=wrappingPaperStyle; message=null}
  14. | Boxed ->
  15. {discriminator= "Boxed"; wrappingPaperStyle=null; message=null}
  16. | WithACard message ->
  17. {discriminator= "WithACard"; wrappingPaperStyle=null; message=message}
  18. // main call
  19. Tree.map fLeaf fNode gift

You can see that the case (Book, Chocolate, etc.) is turned into a discriminator string and the chocolateType is also turned into a string, just
as explained above.

Step 3: Defining a TreeDto

I said above that a good DTO should be a record type. Well we have converted the nodes of the tree, but the tree itself is a union type!
We need to transform the Tree type as well, into say a TreeDto type.

How can we do this? Just as for the gift DTO types, we will create a record type which contains all the data for both cases. We could use a discriminator
field as we did before, but this time, since there are only two choices, leaf and internal node, I’ll just check whether the values are null or not when deserializing.
If the leaf value is not null, then the record must represent the LeafNode case, otherwise the record must represent the InternalNode case.

Here’s the definition of the data type:

  1. /// A DTO that represents a Tree
  2. /// The Leaf/Node choice is turned into a record
  3. [<CLIMutableAttribute>]
  4. type TreeDto<'LeafData,'NodeData> = {
  5. leafData : 'LeafData
  6. nodeData : 'NodeData
  7. subtrees : TreeDto<'LeafData,'NodeData>[] }

As before, the type has the CLIMutableAttribute. And as before, the type has fields to store the data from all possible choices.
The subtrees are stored as an array rather than a seq — this makes the serializer happy!

To create a TreeDto, we use our old friend cata to assemble the record from a regular Tree.

  1. /// Transform a Tree into a TreeDto
  2. let treeToDto tree : TreeDto<'LeafData,'NodeData> =
  3. let fLeaf leafData =
  4. let nodeData = Unchecked.defaultof<'NodeData>
  5. let subtrees = [||]
  6. {leafData=leafData; nodeData=nodeData; subtrees=subtrees}
  7. let fNode nodeData subtrees =
  8. let leafData = Unchecked.defaultof<'NodeData>
  9. let subtrees = subtrees |> Seq.toArray
  10. {leafData=leafData; nodeData=nodeData; subtrees=subtrees}
  11. // recurse to build up the TreeDto
  12. Tree.cata fLeaf fNode tree

Note that in F#, records are not nullable, so I am using Unchecked.defaultof<'NodeData> rather than null to indicate missing data.

Note also that I am assuming that LeafData or NodeData are reference types.
If LeafData or NodeData are ever value types like int or bool, then this approach will break down, because you won’t be able to
tell the difference between a default value and a missing value. In which case, I’d switch to a discriminator field as before.

Alternatively, I could have used an IDictionary. That would be less convenient to deserialize, but would avoid the need for null-checking.

Step 4: Serializing a TreeDto

Finally we can serialize the TreeDto using a JSON serializer.

For this example, I am using the built-in DataContractJsonSerializer so that I don’t need to take
a dependency on a NuGet package. There are other JSON serializers that might be better for a serious project.

  1. #r "System.Runtime.Serialization.dll"
  2. open System.Runtime.Serialization
  3. open System.Runtime.Serialization.Json
  4. let toJson (o:'a) =
  5. let serializer = new DataContractJsonSerializer(typeof<'a>)
  6. let encoding = System.Text.UTF8Encoding()
  7. use stream = new System.IO.MemoryStream()
  8. serializer.WriteObject(stream,o)
  9. stream.Close()
  10. encoding.GetString(stream.ToArray())

Step 5: Assembling the pipeline

So, putting it all together, we have the following pipeline:

  • Transform Gift to GiftDto using giftToDto,

    that is, use Tree.map to go from Tree<GiftContents,GiftDecoration> to Tree<GiftContentsDto,GiftDecorationDto>
  • Transform Tree to TreeDto using treeToDto,

    that is, use Tree.cata to go from Tree<GiftContentsDto,GiftDecorationDto> to TreeDto<GiftContentsDto,GiftDecorationDto>
  • Serialize TreeDto to a JSON string

Here’s some example code:

  1. let goodJson = christmasPresent |> giftToDto |> treeToDto |> toJson

And here is what the JSON output looks like:

  1. {
  2. "leafData@": null,
  3. "nodeData@": {
  4. "discriminator@": "Wrapped",
  5. "message@": null,
  6. "wrappingPaperStyle@": "HappyHolidays"
  7. },
  8. "subtrees@": [
  9. {
  10. "leafData@": null,
  11. "nodeData@": {
  12. "discriminator@": "Boxed",
  13. "message@": null,
  14. "wrappingPaperStyle@": null
  15. },
  16. "subtrees@": [
  17. {
  18. "leafData@": {
  19. "bookTitle@": null,
  20. "chocolateType@": "SeventyPercent",
  21. "discriminator@": "Chocolate",
  22. "price@": 5
  23. },
  24. "nodeData@": null,
  25. "subtrees@": []
  26. }
  27. ]
  28. }
  29. ]
  30. }

The ugly @ signs on the field names are an artifact of serializing the F# record type.
This can be corrected with a bit of effort, but I’m not going to bother right now!

The source code for this example is available at this gist


Example: Deserializing a Tree from JSON

Now that we have created the JSON, what about going the other way and loading it into a Gift?

Simple! We just need to reverse the pipeline:

  • Deserialize a JSON string into a TreeDto.
  • Transform a TreeDto into a Tree to using dtoToTree,

    that is, go from TreeDto<GiftContentsDto,GiftDecorationDto> to Tree<GiftContentsDto,GiftDecorationDto>.
    We can’t use cata for this — we’ll have to create a little recursive loop.
  • Transform GiftDto to Gift using dtoToGift,

    that is, use Tree.map to go from Tree<GiftContentsDto,GiftDecorationDto> to Tree<GiftContents,GiftDecoration>.

Step 1: Deserializing a TreeDto

We can deserialize the TreeDto using a JSON serializer.

  1. let fromJson<'a> str =
  2. let serializer = new DataContractJsonSerializer(typeof<'a>)
  3. let encoding = System.Text.UTF8Encoding()
  4. use stream = new System.IO.MemoryStream(encoding.GetBytes(s=str))
  5. let obj = serializer.ReadObject(stream)
  6. obj :?> 'a

What if the deserialization fails? For now, we will ignore any error handling and let the exception propagate.

Step 2: Transforming a TreeDto into a Tree

To transform a TreeDto into a Tree we recursively loop through the record and its subtrees, turning each one into a InternalNode
or a LeafNode, based on whether the appropriate field is null or not.

  1. let rec dtoToTree (treeDto:TreeDto<'Leaf,'Node>) :Tree<'Leaf,'Node> =
  2. let nullLeaf = Unchecked.defaultof<'Leaf>
  3. let nullNode = Unchecked.defaultof<'Node>
  4. // check if there is nodeData present
  5. if treeDto.nodeData <> nullNode then
  6. if treeDto.subtrees = null then
  7. failwith "subtrees must not be null if node data present"
  8. else
  9. let subtrees = treeDto.subtrees |> Array.map dtoToTree
  10. InternalNode (treeDto.nodeData,subtrees)
  11. // check if there is leafData present
  12. elif treeDto.leafData <> nullLeaf then
  13. LeafNode (treeDto.leafData)
  14. // if both missing then fail
  15. else
  16. failwith "expecting leaf or node data"

As you can see, a number of things could go wrong:

  • What if the leafData and nodeData fields are both null?
  • What if the nodeData field is not null but the subtrees field is null?

Again, we will ignore any error handling and just throw exceptions (for now).

Question: Could we create a cata for TreeDto that would make this code simpler? Would it be worth it?

Step 3: Transforming a GiftDto into Gift

Now we have a proper tree, we can use Tree.map again to convert each leaf and internal node from a DTO to the proper domain type.

That means we need functions that map a GiftContentsDto into a GiftContents and a GiftDecorationDto into a GiftDecoration.

Here’s the complete code — it’s a lot more complicated than going in the other direction!

The code can be grouped as follows:

  • Helper methods (such as strToChocolateType) that convert a string into a proper domain type and throw an exception if the input is invalid.
  • Case converter methods (such as bookFromDto) that convert an entire DTO into a case.
  • And finally, the dtoToGift function itself. It looks at the discriminator field to see which case converter to call,
    and throws an exception if the discriminator value is not recognized.
  1. let strToBookTitle str =
  2. match str with
  3. | null -> failwith "BookTitle must not be null"
  4. | _ -> str
  5. let strToChocolateType str =
  6. match str with
  7. | "Dark" -> Dark
  8. | "Milk" -> Milk
  9. | "SeventyPercent" -> SeventyPercent
  10. | _ -> failwithf "ChocolateType %s not recognized" str
  11. let strToWrappingPaperStyle str =
  12. match str with
  13. | "HappyBirthday" -> HappyBirthday
  14. | "HappyHolidays" -> HappyHolidays
  15. | "SolidColor" -> SolidColor
  16. | _ -> failwithf "WrappingPaperStyle %s not recognized" str
  17. let strToCardMessage str =
  18. match str with
  19. | null -> failwith "CardMessage must not be null"
  20. | _ -> str
  21. let bookFromDto (dto:GiftContentsDto) =
  22. let bookTitle = strToBookTitle dto.bookTitle
  23. Book {title=bookTitle; price=dto.price}
  24. let chocolateFromDto (dto:GiftContentsDto) =
  25. let chocType = strToChocolateType dto.chocolateType
  26. Chocolate {chocType=chocType; price=dto.price}
  27. let wrappedFromDto (dto:GiftDecorationDto) =
  28. let wrappingPaperStyle = strToWrappingPaperStyle dto.wrappingPaperStyle
  29. Wrapped wrappingPaperStyle
  30. let boxedFromDto (dto:GiftDecorationDto) =
  31. Boxed
  32. let withACardFromDto (dto:GiftDecorationDto) =
  33. let message = strToCardMessage dto.message
  34. WithACard message
  35. /// Transform a GiftDto to a Gift
  36. let dtoToGift (giftDto:GiftDto) :Gift=
  37. let fLeaf (leafDto:GiftContentsDto) =
  38. match leafDto.discriminator with
  39. | "Book" -> bookFromDto leafDto
  40. | "Chocolate" -> chocolateFromDto leafDto
  41. | _ -> failwithf "Unknown leaf discriminator '%s'" leafDto.discriminator
  42. let fNode (nodeDto:GiftDecorationDto) =
  43. match nodeDto.discriminator with
  44. | "Wrapped" -> wrappedFromDto nodeDto
  45. | "Boxed" -> boxedFromDto nodeDto
  46. | "WithACard" -> withACardFromDto nodeDto
  47. | _ -> failwithf "Unknown node discriminator '%s'" nodeDto.discriminator
  48. // map the tree
  49. Tree.map fLeaf fNode giftDto

Step 4: Assembling the pipeline

We can now assemble the pipeline that takes a JSON string and creates a Gift.

  1. let goodGift = goodJson |> fromJson |> dtoToTree |> dtoToGift
  2. // check that the description is unchanged
  3. goodGift |> description
  4. // "SeventyPercent chocolate in a box wrapped in HappyHolidays paper"

This works fine, but the error handling is terrible!

Look what happens if we corrupt the JSON a little:

  1. let badJson1 = goodJson.Replace("leafData","leafDataXX")
  2. let badJson1_result = badJson1 |> fromJson |> dtoToTree |> dtoToGift
  3. // Exception "The data contract type 'TreeDto' cannot be deserialized because the required data member 'leafData@' was not found."

We get an ugly exception.

Or what if a discriminator is wrong?

  1. let badJson2 = goodJson.Replace("Wrapped","Wrapped2")
  2. let badJson2_result = badJson2 |> fromJson |> dtoToTree |> dtoToGift
  3. // Exception "Unknown node discriminator 'Wrapped2'"

or one of the values for the WrappingPaperStyle DU?

  1. let badJson3 = goodJson.Replace("HappyHolidays","HappyHolidays2")
  2. let badJson3_result = badJson3 |> fromJson |> dtoToTree |> dtoToGift
  3. // Exception "WrappingPaperStyle HappyHolidays2 not recognized"

We get lots of exceptions, and as as functional programmers, we should try to remove them whenever we can.

How we can do that will be discussed in the next section.

The source code for this example is available at this gist.


Example: Deserializing a Tree from JSON - with error handling

To address the error handling issue, we’re going use the Result type shown below:

  1. type Result<'a> =
  2. | Success of 'a
  3. | Failure of string list

I’m not going to explain how it works here.
If you are not familar with this approach, please read my post or watch my talk on the topic of functional error handling.

Let’s revisit all the steps from the previous section, and use Result rather than throwing exceptions.

Step 1: Deserializing a TreeDto

When we deserialize the TreeDto using a JSON serializer we will trap exceptions and turn them into a Result.

  1. let fromJson<'a> str =
  2. try
  3. let serializer = new DataContractJsonSerializer(typeof<'a>)
  4. let encoding = System.Text.UTF8Encoding()
  5. use stream = new System.IO.MemoryStream(encoding.GetBytes(s=str))
  6. let obj = serializer.ReadObject(stream)
  7. obj :?> 'a
  8. |> Result.retn
  9. with
  10. | ex ->
  11. Result.failWithMsg ex.Message

The signature of fromJson is now string -> Result<'a>.

Step 2: Transforming a TreeDto into a Tree

As before, we transform a TreeDto into a Tree by recursively looping through the record and its subtrees, turning each one into a InternalNode
or a LeafNode. This time, though, we use Result to handle any errors.

  1. let rec dtoToTreeOfResults (treeDto:TreeDto<'Leaf,'Node>) :Tree<Result<'Leaf>,Result<'Node>> =
  2. let nullLeaf = Unchecked.defaultof<'Leaf>
  3. let nullNode = Unchecked.defaultof<'Node>
  4. // check if there is nodeData present
  5. if treeDto.nodeData <> nullNode then
  6. if treeDto.subtrees = null then
  7. LeafNode <| Result.failWithMsg "subtrees must not be null if node data present"
  8. else
  9. let subtrees = treeDto.subtrees |> Array.map dtoToTreeOfResults
  10. InternalNode (Result.retn treeDto.nodeData,subtrees)
  11. // check if there is leafData present
  12. elif treeDto.leafData <> nullLeaf then
  13. LeafNode <| Result.retn (treeDto.leafData)
  14. // if both missing then fail
  15. else
  16. LeafNode <| Result.failWithMsg "expecting leaf or node data"
  17. // val dtoToTreeOfResults :
  18. // treeDto:TreeDto<'Leaf,'Node> -> Tree<Result<'Leaf>,Result<'Node>>

But uh-oh, we now have a Tree where every internal node and leaf is wrapped in a Result. It’s a tree of Results!
The actual ugly signature is this: Tree<Result<'Leaf>,Result<'Node>>.

But this type is useless as it stands — what we really want is to merge all the errors together and return a Result containing a Tree.

How can we transform a Tree of Results into a Result of Tree?

The answer is to use a sequence function which “swaps” the two types.
You can read much more about sequence in my series on elevated worlds.

Note that we could also use the slightly more complicated traverse variant to combine the map and sequence into one step,
but for the purposes of this demonstration, it’s easier to understand if the steps are kept separate.

We need to create our own sequence function for the Tree/Result combination. Luckily the creation of a sequence function
is a mechanical process:

  • For the lower type (Result) we need to define apply and return functions. See here for more details on what apply means.
  • For the higher type (Tree) we need to have a cata function, which we do.
  • In the catamorphism, each constructor of the higher type (LeafNode and InternalNode in this case) is replaced by an equivalent that is “lifted” to the Result type (e.g. retn LeafNode <*> data)

Here is the actual code — don’t worry if you can’t understand it immediately. Luckily, we only need to write it once for each combination
of types, so for any kind of Tree/Result combination in the future, we’re set!

  1. /// Convert a tree of Results into a Result of tree
  2. let sequenceTreeOfResult tree =
  3. // from the lower level
  4. let (<*>) = Result.apply
  5. let retn = Result.retn
  6. // from the traversable level
  7. let fLeaf data =
  8. retn LeafNode <*> data
  9. let fNode data subitems =
  10. let makeNode data items = InternalNode(data,items)
  11. let subItems = Result.sequenceSeq subitems
  12. retn makeNode <*> data <*> subItems
  13. // do the traverse
  14. Tree.cata fLeaf fNode tree
  15. // val sequenceTreeOfResult :
  16. // tree:Tree<Result<'a>,Result<'b>> -> Result<Tree<'a,'b>>

Finally, the actual dtoToTree function is simple — just send the treeDto through dtoToTreeOfResults and then use sequenceTreeOfResult to
convert the final result into a Result<Tree<..>>, which is just what we need.

  1. let dtoToTree treeDto =
  2. treeDto |> dtoToTreeOfResults |> sequenceTreeOfResult
  3. // val dtoToTree : treeDto:TreeDto<'a,'b> -> Result<Tree<'a,'b>>

Step 3: Transforming a GiftDto into a Gift

Again we can use Tree.map to convert each leaf and internal node from a DTO to the proper domain type.

But our functions will handle errors, so they need to map a GiftContentsDto into a Result<GiftContents>
and a GiftDecorationDto into a Result<GiftDecoration>. This results in a Tree of Results again, and so we’ll have to
use sequenceTreeOfResult again to get it back into the correct Result<Tree<..>> shape.

Let’s start with the helper methods (such as strToChocolateType) that convert a string into a proper domain type.
This time, they return a Result rather than throwing an exception.

  1. let strToBookTitle str =
  2. match str with
  3. | null -> Result.failWithMsg "BookTitle must not be null"
  4. | _ -> Result.retn str
  5. let strToChocolateType str =
  6. match str with
  7. | "Dark" -> Result.retn Dark
  8. | "Milk" -> Result.retn Milk
  9. | "SeventyPercent" -> Result.retn SeventyPercent
  10. | _ -> Result.failWithMsg (sprintf "ChocolateType %s not recognized" str)
  11. let strToWrappingPaperStyle str =
  12. match str with
  13. | "HappyBirthday" -> Result.retn HappyBirthday
  14. | "HappyHolidays" -> Result.retn HappyHolidays
  15. | "SolidColor" -> Result.retn SolidColor
  16. | _ -> Result.failWithMsg (sprintf "WrappingPaperStyle %s not recognized" str)
  17. let strToCardMessage str =
  18. match str with
  19. | null -> Result.failWithMsg "CardMessage must not be null"
  20. | _ -> Result.retn str

The case converter methods have to build a Book or Chocolate from parameters that are Results rather than normal values. This is
where lifting functions like Result.lift2 can help.
For details on how this works, see this post on lifting and this one on validation with applicatives.

  1. let bookFromDto (dto:GiftContentsDto) =
  2. let book bookTitle price =
  3. Book {title=bookTitle; price=price}
  4. let bookTitle = strToBookTitle dto.bookTitle
  5. let price = Result.retn dto.price
  6. Result.lift2 book bookTitle price
  7. let chocolateFromDto (dto:GiftContentsDto) =
  8. let choc chocType price =
  9. Chocolate {chocType=chocType; price=price}
  10. let chocType = strToChocolateType dto.chocolateType
  11. let price = Result.retn dto.price
  12. Result.lift2 choc chocType price
  13. let wrappedFromDto (dto:GiftDecorationDto) =
  14. let wrappingPaperStyle = strToWrappingPaperStyle dto.wrappingPaperStyle
  15. Result.map Wrapped wrappingPaperStyle
  16. let boxedFromDto (dto:GiftDecorationDto) =
  17. Result.retn Boxed
  18. let withACardFromDto (dto:GiftDecorationDto) =
  19. let message = strToCardMessage dto.message
  20. Result.map WithACard message

And finally, the dtoToGift function itself is changed to return a Result if the discriminator is invalid.

As before, this mapping creates a Tree of Results, so we pipe the output of the Tree.map through sequenceTreeOfResult

  1. `Tree.map fLeaf fNode giftDto |> sequenceTreeOfResult`

… to return a Result of Tree.

Here’s the complete code for dtoToGift:

  1. open TreeDto_WithErrorHandling
  2. /// Transform a GiftDto to a Result<Gift>
  3. let dtoToGift (giftDto:GiftDto) :Result<Gift>=
  4. let fLeaf (leafDto:GiftContentsDto) =
  5. match leafDto.discriminator with
  6. | "Book" -> bookFromDto leafDto
  7. | "Chocolate" -> chocolateFromDto leafDto
  8. | _ -> Result.failWithMsg (sprintf "Unknown leaf discriminator '%s'" leafDto.discriminator)
  9. let fNode (nodeDto:GiftDecorationDto) =
  10. match nodeDto.discriminator with
  11. | "Wrapped" -> wrappedFromDto nodeDto
  12. | "Boxed" -> boxedFromDto nodeDto
  13. | "WithACard" -> withACardFromDto nodeDto
  14. | _ -> Result.failWithMsg (sprintf "Unknown node discriminator '%s'" nodeDto.discriminator)
  15. // map the tree
  16. Tree.map fLeaf fNode giftDto |> sequenceTreeOfResult

The type signature of dtoToGift has changed — it now returns a Result<Gift> rather than just a Gift.

  1. // val dtoToGift : GiftDto -> Result<GiftUsingTree.Gift>

Step 4: Assembling the pipeline

We can now reassemble the pipeline that takes a JSON string and creates a Gift.

But changes are needed to work with the new error handling code:

  • The fromJson function returns a Result<TreeDto> but the next function in the pipeline (dtoToTree) expects a regular TreeDto as input.
  • Similarly dtoToTree returns a Result<Tree> but the next function in the pipeline (dtoToGift) expects a regular Tree as input.

In both case, Result.bind can be used to solve that problem of mis-matched output/input. See here for a more detailed discussion of bind.

Ok, let’s try deserializing the goodJson string we created earlier.

  1. let goodGift = goodJson |> fromJson |> Result.bind dtoToTree |> Result.bind dtoToGift
  2. // check that the description is unchanged
  3. goodGift |> description
  4. // Success "SeventyPercent chocolate in a box wrapped in HappyHolidays paper"

That’s fine.

Let’s see if the error handling has improved now.
We’ll corrupt the JSON again:

  1. let badJson1 = goodJson.Replace("leafData","leafDataXX")
  2. let badJson1_result = badJson1 |> fromJson |> Result.bind dtoToTree |> Result.bind dtoToGift
  3. // Failure ["The data contract type 'TreeDto' cannot be deserialized because the required data member 'leafData@' was not found."]

Great! We get an nice Failure case.

Or what if a discriminator is wrong?

  1. let badJson2 = goodJson.Replace("Wrapped","Wrapped2")
  2. let badJson2_result = badJson2 |> fromJson |> Result.bind dtoToTree |> Result.bind dtoToGift
  3. // Failure ["Unknown node discriminator 'Wrapped2'"]

or one of the values for the WrappingPaperStyle DU?

  1. let badJson3 = goodJson.Replace("HappyHolidays","HappyHolidays2")
  2. let badJson3_result = badJson3 |> fromJson |> Result.bind dtoToTree |> Result.bind dtoToGift
  3. // Failure ["WrappingPaperStyle HappyHolidays2 not recognized"]

Again, nice Failure cases.

What’s very nice (and this is something that the exception handling approach can’t offer) is that if there is
more than one error, the various errors can be aggregated so that we get a list of all the things that went wrong, rather than just one error at a time.

Let’s see this in action by introducing two errors into the JSON string:

  1. // create two errors
  2. let badJson4 = goodJson.Replace("HappyHolidays","HappyHolidays2")
  3. .Replace("SeventyPercent","SeventyPercent2")
  4. let badJson4_result = badJson4 |> fromJson |> Result.bind dtoToTree |> Result.bind dtoToGift
  5. // Failure ["WrappingPaperStyle HappyHolidays2 not recognized";
  6. // "ChocolateType SeventyPercent2 not recognized"]

So overall, I’d say that’s a success!

The source code for this example is available at this gist.


Summary

We’ve seen in this series how to define catamorphisms, folds, and in this post in particular, how to use them to solve real world problems.
I hope these posts have been useful, and have provided you with some tips and insights that you can apply to your own code.

This series turned out to be a lot longer that I intended, so thanks for making it to the end! Cheers!