An IVR with Neo4j and F# -- Part 2 A
Modeling the IVR domain with algebraic data types
Why am I doing this?
In part 1 I talked about getting started on my graph-based IVR system. I started with modeling the authentication sub-graph. I wrote some Cypher queries to build the graph in Neo4j. In this part I am going to talk about how I modeled the graph in F# using algebraic data types, namely discriminated unions and tuples.
This is my crack at it.
Readify's Neo4jClient
I have used Readify's Neo4jClient library before in C# projects. This is a great library with good documentation and make sense. ( I have also used Py2Neo 2.0, which is really awesome, and Py2Neo 3.0 is even better.) This is my first try using the .NET Neo4jClient in F#. I will give a simple example of doing MERGE.
Nodes
Label
Nodes in Neo4j can have labels which are like node types. I modeled the possible labels in my graph as a discriminated union , like this.
type Label =
| START
| END
| ENTRY
| RETRY
| RESPONSE
Then I can use pattern matching on the label to change how I process the node information, if I want to. At this point I don't have different logic based on the node label, but I could. When I use this union I am just returning a string that is the node label, like this.
let label =
match node.Label with
| START -> "START"
| END -> "END"
| ENTRY -> "ENTRY"
| RETRY -> "RETRY"
| RESPONSE -> "RESPONSE"
Properties
In Neo4j, each node can have properties as well as a label. A node can have any set of arbitrary properties (like a dictionary), of any types. Neo4j is essentially a schemaless database so you can have any properties you like on any node.
To model this I used a Record type. This is a little like a dictionary and it looks like this.
type NodeProperties =
{
Id: int
Title: string
Message: string
Retries: int
}
type IVRNode =
{
Label: Label
Properties: NodeProperties
}
I created a type called IVRNode with a Label field, which is my Label union, and a Properties field, which is itself a Record type, called NodeProperties. I did this to make it easier to access just the properties of the node in the Neo4jClient, as we will see.
Relationships
In Neo4j a relationship is the edge between nodes. It connects your bits of data. Relationships can have a type and properties, like nodes. I chose to model the relationship types as a discriminated union. The relationships won't have properties (for now).
type Relationship =
| GOTO
| SUCCESS
| FAIL
Like node labels, I can use pattern matching on these to control how the relationship is processed. For now I am just returning a string that represents relationship types.
Paths
I created a Path type model sub-graph scenarios. For example
--> -->
(ENTRY)-[:FAIL]->(RETRY)
The sub-graph looks like this:
The Cypher looks like this:
match (a:ENTRY {id: 1})-[r:FAIL]->(b:RETRY {id: 1})
return a, r, b
The Path type is a tuple like this
type Path = IVRNode * Relationship * IVRNode
With this tuple I connect one node to the next with a relationship.
Neo4jClient MERGE
The Neo4jClient is pretty nice and really helps work with Neo4j. There are lots of different options for doing common queries like MATCH, CREATE, and MERGE. To get started, I built out a little function to create a single node. The Neo4jClient is pretty flexible in that you define you queries using a kind of pseudo-Cypher syntax, which is fine, but working with strings can be a little annoying. Here is a sample:
MERGE a single node
let inline (=>) a b = a, box b
let nodeProperties = node.Properties
neo4Client.Cypher
.Merge(sprintf "(%s:%s {id: {id}, title: {title}, message: {message}, retries: {retries} })" "a" label)
.OnCreate()
.Set("a = {nodeProperties}")
.WithParams(dict [
"id" => nodeProperties.Id
"title" => nodeProperties.Title
"message" => nodeProperties.Message
"retries" => nodeProperties.Retries
"nodeProperties" => nodeProperties
])
.ExecuteWithoutResults()
Let's talk about two parts of this thing.
.Merge.
"a" is an alias in the query like you would write in Cypher. Notice that this looks a lot like Cypher node syntax.
dict []
This is a dictionary and this is how we do anonymous objects in F#. First I defined an operator overload (=>) like this. This is a nice trick I got from Stack Overflow.
let inline (=>) a b = a, box b
.WithParams takes an object in C#:
object parameters
The dictionary are key-value pairs of the node properties with one last pair which is the actual record type that has all the properties in it.
.WithParams(dict [
"id" => nodeProperties.Id
"title" => nodeProperties.Title
"message" => nodeProperties.Message
"retries" => nodeProperties.Retries
"nodeProperties" => nodeProperties
])
My whole module looks like this:
namespace GraphIVR.Infrastructure
module BuildGraph =
open System
open GraphIVR.Core.Models
open Neo4jClient
open Neo4jClient.Cypher
open FSharp.Configuration
type Neo4jAppSettings = AppSettings<"App.config">
let neo4Client = new GraphClient(new Uri(Neo4jAppSettings.ConnectionStrings.Neo4j), "neo4j", "CV1g:[dluwfX");
let createNode (node:IVRNode) =
let label =
match node.Label with
| START -> "START"
| END -> "END"
| ENTRY -> "ENTRY"
| RETRY -> "RETRY"
| RESPONSE -> "RESPONSE"
neo4Client.Connect()
let cypherNode label alias =
sprintf "(%s:%s {id: {id}, title: {title}, message: {message}, retries: {retries} })" alias label
let inline (=>) a b = a, box b
let nodeProperties = node.Properties
neo4Client.Cypher
.Merge(cypherNode label alias)
.OnCreate()
.Set("a = {nodeProperties}")
.WithParams(dict [
"id" => nodeProperties.Id
"title" => nodeProperties.Title
"message" => nodeProperties.Message
"retries" => nodeProperties.Retries
"nodeProperties" => nodeProperties
])
.ExecuteWithoutResults()
I refactored a bit to make it nicer.
Tests
I wrote a test. Yep. And it passed. Because this is F# and if it compiles it probably works.
module BuildGraphTests
open System
open GraphIVR.Core.Models
open GraphIVR.Infrastructure
open Xunit
open FsUnit.Xunit
[<Fact>]
let ``Can create a START node`` () =
let node =
{
Label = Label.START
Properties =
{
Id = 78878
Title = "TEST"
Message = "TEST"
Retries = 0
}
}
BuildGraph.createNode(node)
Thoughts
Man, did it take me a while to work this out. At first I was trying out Neo4jClient.Extensions, which provides a fluent API so you don't have to write pseudo-Cypher. That was nice and all, but it was becoming all a bit messy. The model was complicated and turning a bit redundant. So I said, Forget it, let's go back to basics.
This was fun and a lot easier when I went back to doing it more functionally. I think I have a pretty good base to work from.
This all seems like pretty basic stuff, but maybe that is just the power of a functional language? I will be adding more to this later on as I build out the rest of the system.
Links
An IVR with Neo4j and F# -- Part 1
Github repo
Readify's Neo4jClient
Neo4jExtensions -- Highly recommended if using C#
Neo4j
Algebraic Data Types
Cypher graph query language
Full Stack .NET Programmer and Ham