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.jsonfrom Tres, changing the database toO2F5;Data.fsfrom Tres (changes described below); andCollection.fs,Domain.fs, andIndexes.fsfrom Quatro, changing the module namespaces toCinco.
The section of Cinco.fsproj that specifies the build order should look like this:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
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: |
|
We've seen DUs like this already; however, with this definition, our DataConfig record now becomes dead simple:
1:
|
|
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: |
|
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. -
ObjectandString(andNumber, though we don't have any) are Chiron types (cases of a DU, actually), so ourmatchstatement 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: |
|
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
storeparameter will be called withnew DocumentStore (). -
For each item in the collection, you run it through the function, producing a new state. Since
DocumentStoreis 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: |
|
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
IDocumentStoreis 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
IDocumentStoreand returns aPost. - 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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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:
|
|
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:
|
|
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:
|
|
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.
type Categories_ByWebLogIdAndSlug =
inherit AbstractJavaScriptIndexCreationTask
new : unit -> Categories_ByWebLogIdAndSlug
--------------------
new : unit -> Categories_ByWebLogIdAndSlug
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
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>
val string : value:'T -> string
--------------------
type string = System.String
| Url of string
| Database of string
{Parameters: ConfigParameter list;}
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
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>
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
...
union case Json.String: string -> Json
--------------------
module String
from Microsoft.FSharp.Core
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
namespace Raven.Client.Documents.Indexes
--------------------
module Indexes
from Cinco
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(assemblyToScan: System.Reflection.Assembly, store: IDocumentStore,?conventions: Conventions.DocumentConventions,?database: string) : unit
from Cinco
interface
abstract member Store : IDocumentStore
end
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
...
type AutoOpenAttribute =
inherit Attribute
new : unit -> AutoOpenAttribute
new : path:string -> AutoOpenAttribute
member Path : string
--------------------
new : unit -> AutoOpenAttribute
new : path:string -> AutoOpenAttribute
from Cinco
module Data
from Cinco
--------------------
namespace Microsoft.FSharp.Data
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, encoding: System.Text.Encoding) : string
module DataConfig
from Cinco.Data
--------------------
type DataConfig =
{Parameters: ConfigParameter list;}
--------------------
new : ?tupleAsHeterogeneousArray:bool * ?usePropertyFormatterForValues:bool -> CompactUnionJsonConverter