objects |> functions


Cinco - Step 3

As with our previous versions, we need to add RavenDb.Client to paket.references, and we'll also need Microsoft.FSharpLu.Json; run paket install after those are added. We'll also utilize the following files from prior projects:

  • data-config.json from Tres, changing the database to O2F5;
  • Data.fs from Tres (changes described below); and
  • Collection.fs, Domain.fs, and Indexes.fs from Quatro, changing the module namespaces to Cinco.

The section of Cinco.fsproj that specifies the build order should look like this:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
<ItemGroup>
  <Compile Include="Collection.fs" />
  <Compile Include="Domain.fs" />
  <Compile Include="Indexes.fs" />
  <Compile Include="Data.fs" />
  <Compile Include="App.fs" />
  <Content Include="data-config.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
</ItemGroup>

Parsing data.config (and More)

Up to this point, we've used Json.NET to parse data-config.json. There's nothing wrong with that approach, but we'll implement our JSON parsing a different way in this project, using a library from the same people who bring us Freya called Chiron (pronounced "KY-ron"). Of course, to be able to use it, we have to pull it in as a dependency. Add nuget Chiron to paket.dependencies, add Chiron to paket.references, and run paket install.

We'll utilize a discriminated union to declare the supported parameters we allow to be set. Open Data.fs, change the first line to module Cinco.Data, and delete the Collection module (we included that in another file). Then, we'll add a DU with our expected parameters.

1: 
2: 
3: 
4: 
5: 
6: 
  open Chiron
  open Raven.Client.Documents

  type ConfigParameter =
    | Url      of string
    | Database of string

We've seen DUs like this already; however, with this definition, our DataConfig record now becomes dead simple:

1: 
  type DataConfig = { Parameters : ConfigParameter list }

We'll make a module to let us parse this using Chiron. In the module, we'll also include code to configure RavenDB's DocumentStore object from the configuration.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
  module DataConfig =
    let fromJson json =
      match Json.parse json with
      | Object config ->
          { Parameters =
              config
              |> Map.toList
              |> List.map (fun item ->
                  match item with
                  | "Url",      String x -> Url x
                  | "Database", String x -> Database x
                  | key, value -> invalidOp <| sprintf "Unexpected RavenDB config parameter %s (value %A)" key value)
            }
      | _ -> { Parameters = [] }

Before we continue, let's take a look at this; there are several new concepts here.

  • In other versions, if the JSON didn't parse, we raised an exception, but that was about it. In this one, if the JSON doesn't parse, we get a configuration that will end up making no changes to the default DocumentStore (which will fail on connect, because we haven't specified any URLs). Maybe this is better, maybe not, but it demonstrates that there is a way to handle bad JSON other than an exception.
  • Object and String (and Number, though we don't have any) are Chiron types (cases of a DU, actually), so our match statement uses the destructuring form to "unwrap" the DU's inner value.
  • This version will raise an exception if we attempt to set an option that we do not recognize (something like "Databsae" - not that anyone I know would ever type it like that...).

So, it's more code than JsonConvert.fromJson, but it gives us control over the deserialization. If we get an unexpected parameter, our exception tells what that parameter is. We could also write this in such a way as to raise an exception if the parsed JSON doesn't include a Url parameter. And, while the parsing of JSON is still in a black box, how we handle each value is not.

Moving on to configuring the DocumentStore:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
    let configureStore config store  =
      config.Parameters
      |> List.fold
          (fun (stor : DocumentStore) par -> 
              match par with
              | Url url -> stor.Urls <- [| url |]; stor
              | Database db -> stor.Database <- db; stor)
          store

This demonstrates a powerful concept in functional programming (not just F#), the fold function. All F# collection types' modules define fold for them (and, though the parameter order is different, LINQ defines a similar extension method on the IEnumerable types called Aggregate - you can even use this concept in C# and VB.NET). The concept behind a fold is not terribly difficult to grasp:

  • Start with a known state; in our case, when we call this, the store parameter will be called with new DocumentStore ().
  • For each item in the collection, you run it through the function, producing a new state. Since DocumentStore is a .NET class, we mutate one of its properties (depending on what parameter we're processing), and return the same object. It doesn't have to be the same object, it just has to be the same type.
  • The result is the modified state.

In F# terms, the signature is ('State -> 'T -> 'State) -> 'State -> 'T list -> 'State. The function just below List.fold has the signature of the first function; stor is our state, and par is the parameter we're processing. We pass store as the second parameter, and use the |> operator to provide the collection. One advantage to composing it this way is that the compiler can determine the type of 'T, so we do not have to specify it. We could include config.Parameters as the last parameter to List.fold, but we'd have to define the type for par, as the compiler could not infer it.

Why would you want to write this in this way? Let's remember what App.fs looked like in Quatro:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let config = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
let cfg = config.GetSection "RavenDB"
let store = new DocumentStore (Urls = [| cfg.["Url"] |], Database = cfg.["Database"])
store.Conventions.CustomizeJsonDeserializer <-
  fun x ->
      x.Converters.Add (CompactUnionJsonConverter ())
svc.AddSingleton (store.Initialize ()) |> ignore

While we won't be adding it to a DI container, the code from Quatro is procedural code; build the provider, get the section, create the store, add the serializer - it's a step-by-step how-to guide for getting from zero to an initialized connection. Conversely, configureStore is a description of the transformation that will be applied to a new store, one of the hallmarks of functional thinking. Within functional programming, a "pure" function is one that has no side effects; every time it is called with the same input, it produces the same return value, and does not rely on nor change anything else. While some may not consider configureStore a pure function due to its use of mutation, from a logic standpoint it is, as it will always make the same changes to whatever DocumentStore is passed. It's an isolated transformation.

Speaking of thinking functionally - time to replace our DI container!

Dependency Injection with Functional Style

One of the concepts that dependency injection is said to implement is "inversion of control;" rather than an object compiling and linking a dependency at compile time, it compiles against an interface, and the concrete implementation is provided at runtime. (This is a bit of an oversimplification, but it's the basic gist.) If you've ever done non-DI/non-IoC work, and learned DI, you've adjusted your thinking from "what do I need" to "what will I need". In the functional world, this is done through a concept called the Reader monad. The basic concept is as follows:

  • We have a set of dependencies that we establish and set up in our code.
  • We a process with a dependency that we want to be injected (in our case, our IDocumentStore is one such dependency).
  • We construct a function that requires this dependency, and returns the result we seek. Though we won't see it in this step, it's easy to imagine a function that requires an IDocumentStore and returns a Post.
  • We create a function that, given our set of dependencies, will extract the one we need for this process.
  • We run our dependencies through the extraction function, to the dependent function, which takes the dependency and returns the result.

Confused yet? Me too - let's look at code instead. Let's create Dependencies.fs and add it to the build order above Domain.fs. This write-up won't expound on every line in this file, but we'll hit the highlights to see how all this comes together. ReaderM is a generic class, where the first type is the dependency we need, and the second type is the type of our result.

After that (which will come back to in a bit), we'll create our dependencies, and a function to extract an IDocumentStore from it.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
open Raven.Client.Documents

type IDependencies =
  abstract Store : IDocumentStore

[<AutoOpen>]
module DependencyExtraction =
  
  let getStore (deps : IDependencies) = deps.Store

Our IDependencies are pretty lean right now, but that's OK; we'll flesh it out in future steps. We also wrote a dead-easy function to get the store; the signature is literally IDependencies -> IDocumentStore. No ReaderM in sight - yet!

Now that we have a dependency "set" (of one), we need to go to App.fs and make sure we actually have a concrete instance of this for runtime. Change namespace Cinco to module Cinco.App, and add this just before main function:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
  open Data
  open Microsoft.FSharpLu.Json
  open Raven.Client.Documents
  open System.IO

  let cfg = (File.ReadAllText >> DataConfig.fromJson) "data-config.json"
  let deps = {
    new IDependencies with
      member __.Store
        with get () =
          let store = lazy (
            let stor = DataConfig.configureStore cfg (new DocumentStore ())
            stor.Conventions.CustomizeJsonDeserializer <-
              fun x ->
                  x.Converters.Add (CompactUnionJsonConverter ())
            stor.Initialize ()
            )
          store.Force()
      }

Here, we're using lazy to do this only once and only-on-demand, then we turn around and demand it. If you're thinking this sounds a lot like a singleton - your thinking is superb! That's exactly what we're doing here. We're also using F#'s inline interface declaration to create an implementation without creating a concrete class in which it is held. If you hover over deps above, you'll see that its type is IDependencies; even though we're using a traditional .NET interface, we won't have to cast it to its interface type to use it, as it's an anonymous concrete implementation.

Maybe being our own IoC container isn't so bad! There is one piece missing, though, compared to our other implementations; we aren't yet making any calls to RavenDB to make sure our indexes exist. Let's add that as a function within Data.fs, at the bottom of the file:

1: 
2: 
3: 
4: 
  open Indexes

  let ensureIndexes store =
    IndexCreation.CreateIndexes (typeof<Categories_ByWebLogIdAndSlug>.Assembly, store)

Now, let's take a stab at actually pulling our store out of our dependencies, so we can use it to make sure our indexes exist. At the top of main:

1: 
2: 
3: 
    let checkIndexes store = Data.ensureIndexes store
    let start = liftDep getStore checkIndexes
    start |> run deps

If we're letting the types be our guide, how are we doing with these? checkIndexes has the signature IDocumentStore -> unit, start has the signature ReaderM<IDependencies, unit>, and the third line is simply unit (which we can tell because the compiler isn't telling us we need to ignore the value or assign it). And, were we to run it, it would work, but... it's not really composable. Do we really want to have to define two extra variables every time we do something that requires a dependency? Of course not.

Notice that the signature for checkIndexes is the same as Data.ensureIndexes; checkIndexes is redundant. And, there's no need to establish a variable for start either; we can just pipe our entire expression into run deps.

1: 
2: 
    liftDep getStore Data.ensureIndexes
    |> run deps

It works! We set up our dependencies, we composed a function using a dependency, and we used a Reader monad to make it all work. But, how did it work? Given what we just learned above, let's look at the steps; we're coders, not magicians.

First up, liftDeps.

1: 
  let liftDep (proj : 'd2 -> 'd1) (rm : ReaderM<'d1, 'output>) : ReaderM<'d2, 'output> = proj >> rm

The proj parameter is defined as a function that takes one value and returns another one. The rm parameter is a Reader monad that takes the return value of proj, and returns a Reader monad that takes the parameter value of proj and returns an output type. We passed getStore as the proj parameter, and its signature is IDependencies -> IDocumentStore; the second parameter was a function with the signature IDocumentStore -> unit. Where does this turn into a ReaderM? Why, the definition, of course!

1: 
type ReaderM<'d, 'out> = 'd -> 'out

Remember the time we spent discussing the >> operator? ReaderM is an alias for a one-parameter function, and liftDep uses it to compose one function with another, returning a one-parameter function. In concrete types for liftDep's generics in our example, using getStore for proj means that 'd1 is IDocumentStore and 'd2 is IDependencies. (Note that proj is 'd2 -> 'd1; that's why its parameters are reversed from getStore's.) When we passed Data.ensureIndexes as rm, its 'd1 is IDocumentStore, and 'output is unit. When these two functions are composed, we end up with a function that requires IDependencies ('d2) and returns unit ('output); this matches the definition of liftDep's output type, as ReaderM<'d2, 'output> is an alias for 'd2 -> 'output.

If your head is spinning, get it stabilized, then read through that again. It can be quite complex - until it clicks. Then, it's like a light bulb goes off above your head. "Oh, it's called a reader monad because it shows us how to read the part we need from an object!" That's exactly what it does. We need a document store, and this other object has it; ReaderM lets us specify how to "read" (obtain) that dependency. We are free to write as many IDocumentStore-requiring functions as we want, and we'll be able to use liftDep and getStore to change those into IDependencies-requiring functions that return the same thing.

Then, we can run them! run is defined as:

1: 
  let run dep (rm : ReaderM<_,_>) = rm dep

This is way easier than what we've seen up to this point. It takes an object and a ReaderM, and applies the object to the first parameter of the monad. By |>ing the ReaderM<IDependencies, unit> to it, and providing our IDependencies instance, we receive the result; the reader has successfully encapsulated all the functions below it. From this point on, we'll just make sure our types are correct, and we'll be able to utilize not only an IDocumentStore for data manipulation, but any other dependencies we may need to define.

Take a deep breath. Step 3 is done, and not only does it work, we understand why it works.


Back to Step 3

namespace Raven
namespace Raven.Client
namespace Raven.Client.Documents
namespace Raven.Client.Documents.Indexes
namespace System
namespace System.Collections
namespace System.Collections.Generic
Multiple items
type Categories_ByWebLogIdAndSlug =
  inherit AbstractJavaScriptIndexCreationTask
  new : unit -> Categories_ByWebLogIdAndSlug

--------------------
new : unit -> Categories_ByWebLogIdAndSlug
val this : Categories_ByWebLogIdAndSlug
Multiple items
type AbstractJavaScriptIndexCreationTask =
  inherit AbstractIndexCreationTask
  member CreateIndexDefinition : unit -> IndexDefinition
  member Fields : Dictionary<string, IndexFieldOptions> with get, set
  member IsMapReduce : bool
  member Maps : HashSet<string> with get, set

--------------------
AbstractJavaScriptIndexCreationTask() : AbstractJavaScriptIndexCreationTask
property AbstractJavaScriptIndexCreationTask.Maps: HashSet<string>
Multiple items
type HashSet<'T> =
  new : unit -> HashSet<'T> + 3 overloads
  member Add : item:'T -> bool
  member Clear : unit -> unit
  member Comparer : IEqualityComparer<'T>
  member Contains : item:'T -> bool
  member CopyTo : array:'T[] -> unit + 2 overloads
  member Count : int
  member ExceptWith : other:IEnumerable<'T> -> unit
  member GetEnumerator : unit -> Enumerator<'T>
  member GetObjectData : info:SerializationInfo * context:StreamingContext -> unit
  ...
  nested type Enumerator

--------------------
HashSet() : HashSet<'T>
HashSet(comparer: IEqualityComparer<'T>) : HashSet<'T>
HashSet(collection: IEnumerable<'T>) : HashSet<'T>
HashSet(collection: IEnumerable<'T>, comparer: IEqualityComparer<'T>) : HashSet<'T>
Multiple items
val string : value:'T -> string

--------------------
type string = System.String
namespace Microsoft.FSharp.Data
module Chiron
type ConfigParameter =
  | Url of string
  | Database of string
union case ConfigParameter.Url: string -> ConfigParameter
union case ConfigParameter.Database: string -> ConfigParameter
type DataConfig =
  {Parameters: ConfigParameter list;}
DataConfig.Parameters: ConfigParameter list
type 'T list = List<'T>
val fromJson : json:string -> DataConfig
val json : string
Multiple items
module Json

from Chiron.Mapping

--------------------
module Json

from Chiron.Formatting

--------------------
module Json

from Chiron.Parsing

--------------------
module Json

from Chiron.Optics

--------------------
module Json

from Chiron.Functional

--------------------

--------------------
type Json<'a> = Json -> JsonResult<'a> * Json
val parse : (string -> Json)
union case Json.Object: Map<string,Json> -> Json
val config : Map<string,Json>
Multiple items
module Map

from Microsoft.FSharp.Collections

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

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val toList : table:Map<'Key,'T> -> ('Key * 'T) list (requires comparison)
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    member Tail : 'T list
    ...
val map : mapping:('T -> 'U) -> list:'T list -> 'U list
val item : string * Json
Multiple items
union case Json.String: string -> Json

--------------------
module String

from Microsoft.FSharp.Core
val x : string
val key : string
val value : Json
val invalidOp : message:string -> 'T
val sprintf : format:Printf.StringFormat<'T> -> 'T
val configureStore : config:DataConfig -> store:DocumentStore -> DocumentStore
val config : DataConfig
val store : DocumentStore
val fold : folder:('State -> 'T -> 'State) -> state:'State -> list:'T list -> 'State
val stor : DocumentStore
Multiple items
type DocumentStore =
  inherit DocumentStoreBase
  new : unit -> DocumentStore
  member AggressivelyCacheFor : cacheDuration:TimeSpan * ?database:string -> IDisposable + 1 overload
  member BulkInsert : ?database:string * ?token:CancellationToken -> BulkInsertOperation
  member Changes : ?database:string -> IDatabaseChanges + 1 overload
  member DisableAggressiveCaching : ?database:string -> IDisposable
  member Dispose : unit -> unit
  member GetLastDatabaseChangesStateException : ?database:string * ?nodeTag:string -> Exception
  member GetRequestExecutor : ?database:string -> RequestExecutor
  member Identifier : string with get, set
  member Initialize : unit -> IDocumentStore
  ...

--------------------
DocumentStore() : DocumentStore
val par : ConfigParameter
val url : string
property DocumentStoreBase.Urls: string []
val db : string
property DocumentStoreBase.Database: string
val config : obj
val cfg : obj
val store : obj
val ignore : value:'T -> unit
Multiple items
namespace Raven.Client.Documents.Indexes

--------------------
module Indexes

from Cinco
val ensureIndexes : store:IDocumentStore -> unit
val store : IDocumentStore
type IndexCreation =
  static member CreateIndexes : assemblyToScan:Assembly * store:IDocumentStore * ?conventions:DocumentConventions * ?database:string -> unit + 1 overload
  static member CreateIndexesAsync : assemblyToScan:Assembly * store:IDocumentStore * ?conventions:DocumentConventions * ?database:string * ?token:CancellationToken -> Task + 1 overload
IndexCreation.CreateIndexes(indexes: System.Collections.Generic.IEnumerable<AbstractIndexCreationTask>, store: IDocumentStore,?conventions: Conventions.DocumentConventions,?database: string) : unit
IndexCreation.CreateIndexes(assemblyToScan: System.Reflection.Assembly, store: IDocumentStore,?conventions: Conventions.DocumentConventions,?database: string) : unit
val typeof<'T> : System.Type
type ReaderM<'d,'out> = 'd -> 'out
val run : dep:'a -> rm:ReaderM<'a,'b> -> 'b
val dep : 'a
val rm : ReaderM<'a,'b>
val liftDep : proj:('d2 -> 'd1) -> rm:ReaderM<'d1,'output> -> ReaderM<'d2,'output>
val proj : ('d2 -> 'd1)
val rm : ReaderM<'d1,'output>
module Reader

from Cinco
type IDependencies =
  interface
    abstract member Store : IDocumentStore
  end
type IDocumentStore =
  inherit IDisposalNotification
  inherit IDisposable
  member AggressivelyCache : ?database:string -> IDisposable
  member AggressivelyCacheFor : cacheDuration:TimeSpan * ?database:string -> IDisposable + 1 overload
  member BulkInsert : ?database:string * ?token:CancellationToken -> BulkInsertOperation
  member Certificate : X509Certificate2
  member Changes : ?database:string -> IDatabaseChanges + 1 overload
  member Conventions : DocumentConventions
  member Database : string with get, set
  member DisableAggressiveCaching : ?database:string -> IDisposable
  member ExecuteIndex : task:AbstractIndexCreationTask * ?database:string -> unit
  member ExecuteIndexAsync : task:AbstractIndexCreationTask * ?database:string * ?token:CancellationToken -> Task
  ...
Multiple items
type AutoOpenAttribute =
  inherit Attribute
  new : unit -> AutoOpenAttribute
  new : path:string -> AutoOpenAttribute
  member Path : string

--------------------
new : unit -> AutoOpenAttribute
new : path:string -> AutoOpenAttribute
val getStore : deps:IDependencies -> IDocumentStore
val deps : IDependencies
property IDependencies.Store: IDocumentStore
module App

from Cinco
Multiple items
module Data

from Cinco

--------------------
namespace Microsoft.FSharp.Data
namespace Microsoft
namespace Microsoft.FSharpLu
namespace Microsoft.FSharpLu.Json
namespace System.IO
val cfg : DataConfig
type File =
  static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
  static member AppendAllText : path:string * contents:string -> unit + 1 overload
  static member AppendText : path:string -> StreamWriter
  static member Copy : sourceFileName:string * destFileName:string -> unit + 1 overload
  static member Create : path:string -> FileStream + 3 overloads
  static member CreateText : path:string -> StreamWriter
  static member Decrypt : path:string -> unit
  static member Delete : path:string -> unit
  static member Encrypt : path:string -> unit
  static member Exists : path:string -> bool
  ...
File.ReadAllText(path: string) : string
File.ReadAllText(path: string, encoding: System.Text.Encoding) : string
Multiple items
module DataConfig

from Cinco.Data

--------------------
type DataConfig =
  {Parameters: ConfigParameter list;}
val store : Lazy<IDocumentStore>
property DocumentStoreBase.Conventions: Conventions.DocumentConventions
property Conventions.DocumentConventions.CustomizeJsonDeserializer: System.Action<Newtonsoft.Json.JsonSerializer>
val x : Newtonsoft.Json.JsonSerializer
property Newtonsoft.Json.JsonSerializer.Converters: Newtonsoft.Json.JsonConverterCollection
System.Collections.ObjectModel.Collection.Add(item: Newtonsoft.Json.JsonConverter) : unit
Multiple items

--------------------
new : ?tupleAsHeterogeneousArray:bool * ?usePropertyFormatterForValues:bool -> CompactUnionJsonConverter
DocumentStore.Initialize() : IDocumentStore
member System.Lazy.Force : unit -> 'T
val main : 'a -> int
val checkIndexes : (IDocumentStore -> unit)
val start : ReaderM<IDependencies,unit>
Fork me on GitHub