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 toO2F5
;Data.fs
from Tres (changes described below); andCollection.fs
,Domain.fs
, andIndexes.fs
from 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. -
Object
andString
(andNumber
, though we don't have any) are Chiron types (cases of a DU, actually), so ourmatch
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: |
|
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 withnew 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: |
|
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 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