Quatro - Step 3
As with our previous versions, we'll start by adding the RethinkDB driver to project.json
; we'll also bring the
data-config.json
file from Dos/Tres into this project, changing the database name to O2F4
. Follow the
instructions for Tres up though the point where it says "we'll create a file Data.fs
".
Parsing data.config
We'll use Data.fs
in this project as well, but we'll do things a bit more functionally. We'll use
Chiron to parse the JSON file, and we'll set up a discriminated union (DU) for our
configuration parameters.
First, to be able to use Chiron, we'll need the package. Add the following line within the dependencies
section:
1:
|
|
Then, we'll start Data.fs
with our DU.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
This DU looks a bit different than the single-case DUs or enum-style DUs that
we made in step 2. This is a full-fledged DU with 5 different types, 3 strings and 2 integers.
The DataConfig
record now becomes dead simple:
1:
|
|
We'll populate that using Chiron's Json.parse
function.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: |
|
There is a lot to learn in these lines.
- Before, 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 default connection. Maybe this is better, maybe not, but it demonstrates that there is a way to handle bad JSON other than an exception.
-
Object
,String
, andNumber
are Chiron types (cases of a DU, actually), so ourmatch
statement uses the destructuring form to "unwrap" the DU's inner value. ForString
,x
is a string, and forNumber
,x
is a decimal (that's why we run it throughint
to make our DUs. - 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...).
Now, we'll adapt the CreateConnection ()
function to read this new configuration representation:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
Our folder function utilizes a match
on our ConfigParameter
DU. Each time through, it will return a modified
version of the builder
parameter, because one of them will match. We then create our builder by folding the
parameter, using R.Connection ()
as our beginning state, then return its Connect ()
method.
For now, let's copy the rest of Data.fs
from Tres to Quatro - this gives us the table constants and the
table/index initialization code.
Dependency Injection: 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 example, our
IConnection
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
IConnection
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
Entities.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
IConnection
from it.
1: 2: 3: 4: 5: 6: 7: |
|
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 connection; the signature is literally IDependencies -> IConnection
. No ReaderM
funkiness 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. Add this just below the module declaration:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
Here, we're using lazy
to do this once-only-and-only-on-demand, then we turn around and pretty much demand it. If
you're thinking this sounds a lot like singletons - 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.
Maybe being our own IoC container isn't so bad! Now, let's take a stab at actually connection, and running the
EstablishEnvironment
function on startup. At the top of main
:
1: 2: 3: |
|
But wait - we don't have a Database
property on our data config; our configuration is just a list of
ConfigParameter
selections. No worries, though; we can expose a database property on it pretty easily.
1: 2: 3: 4: 5: 6: |
|
OK - now our red squiggly lines are gone. Now, if Jiminy Cricket had written F#, he would have told Pinocchio "Let the
types be your guide". So, how are we doing with these? initDb
has the signature IConnection -> unit
, start
has
the signature ReaderM<IDependencies, unit>
, and the third line is simply unit
. And, were we to run it, it would
work, but... it's not really composable.
Creating extension methods on objects works great in C#-land, and as we've seen, it works the same way in F#-land.
However, in the case where we want to write functions that expect an IConnection
and return our expected result,
extension methods are not what we need. Let's change our AutoOpen
ed DataExtensions
module to something like this:
1: 2: 3: 4: 5: |
|
Now, we have a function with the signature string -> IConnection -> Async<unit>
. This gets us close, but we still
have issues on either side of that signature. On the front, if we were just hard-coding the database name, we could
drop the string parameter, and we'd have our IConnection
as the first parameter. On the return value, we will need
to run the Async
workflow (remember, in F#, they're not started automatically); we need unit
, not Async<unit>
.
We'll use two key F# concepts to fix this up. Currying (also known as partial application) allows us to look at every
return value that isn't the result as a function that's one step closer. Looking at our signature above, you could
express it in English as "a function that takes a string, and returns a function that takes an IConnection
and
returns an Async
workflow." So, to get a function that starts with an IConnection
, we just provide the database
name.
1:
|
|
The signature for almost
is IConnection -> Async<unit>
. Just what we want. For the latter, we use composition.
This is a word that can be used, for example, to describe the way the collection modules expect the collection as the
final parameter, allowing the output of one to be piped, using the |>
operator, to the input of the next. The other
is with the >>
operator, which says to use the output of the first function as the input of the second function,
creating a single function from the two. This is the one we'll use to run our Async
workflow.
1:
|
|
The signature for money
is now IConnection -> unit
, just like we need.
Now, let's revisit initDb
above. Since we don't need the IConnection
as a parameter, we can change that definition
to the same thing we have for money
above. And, since we don't need the parameter, we can just inline the call after
getConn
; we'll just need to wrap the expression in parentheses to indicate that it's a function on its own. And, we
don't need the definition of start
anymore 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 getConn
as the proj
parameter, and its signature is
IDependencies -> IConnection
; the second parameter was a function with the signature IConnection -> unit
. Where
does this turn into a ReaderM
? Why, the definition, of course!
1:
|
|
So, liftDep
derived the expected ReaderM
type from getConn
; 'd1
is IDependencies
and 'd2
is IConnection
.
This means that the next parameter should be a function which takes an IConnection
and returns the output of the type
we expect. Since we pass in IConnection -> unit
, output
is unit
. When all is said and done, if we were to
assign a value to the top line, we would end up with ReaderM<IDependencies, unit>
.
Now, to run it. 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 IConnection
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.
| Hostname of string
| Port of int
| AuthKey of string
| Timeout of int
| Database of string
Full name: Quatro.ConfigParameter
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
Full name: Quatro.ReaderM<_,_>
Full name: Quatro.Reader.run
Full name: Quatro.Reader.liftDep
from Quatro
{Parameters: ConfigParameter list;}
member CreateConnection : unit -> IConnection
member Database : string
static member FromJson : json:'a -> DataConfig
Full name: Quatro.DataConfig
Full name: Microsoft.FSharp.Collections.list<_>
Full name: Quatro.DataConfig.FromJson
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
Full name: Chiron.Functional.Json<_>
type Object =
new : unit -> obj
member Equals : obj:obj -> bool
member GetHashCode : unit -> int
member GetType : unit -> Type
member ToString : unit -> string
static member Equals : objA:obj * objB:obj -> bool
static member ReferenceEquals : objA:obj * objB:obj -> bool
Full name: System.Object
--------------------
Object() : unit
module Map
from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> =
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
override Equals : obj -> bool
member Remove : key:'Key -> Map<'Key,'Value>
...
Full name: Microsoft.FSharp.Collections.Map<_,_>
--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
Full name: Microsoft.FSharp.Collections.Map.toList
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
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
static member Cons : head:'T * tail:'T list -> 'T list
static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
Full name: Microsoft.FSharp.Collections.List.map
type String =
new : value:char -> string + 7 overloads
member Chars : int -> char
member Clone : unit -> obj
member CompareTo : value:obj -> int + 1 overload
member Contains : value:string -> bool
member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
member EndsWith : value:string -> bool + 2 overloads
member Equals : obj:obj -> bool + 2 overloads
member GetEnumerator : unit -> CharEnumerator
member GetHashCode : unit -> int
...
Full name: System.String
--------------------
String(value: nativeptr<char>) : unit
String(value: nativeptr<sbyte>) : unit
String(value: char []) : unit
String(c: char, count: int) : unit
String(value: nativeptr<char>, startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit
String(value: char [], startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : unit
Full name: Microsoft.FSharp.Core.Operators.raise
type InvalidOperationException =
inherit SystemException
new : unit -> InvalidOperationException + 2 overloads
Full name: System.InvalidOperationException
--------------------
InvalidOperationException() : unit
InvalidOperationException(message: string) : unit
InvalidOperationException(message: string, innerException: exn) : unit
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
Full name: Quatro.DataConfig.Database
Full name: Microsoft.FSharp.Collections.List.filter
Full name: Microsoft.FSharp.Collections.List.tryHead
type RethinkDBConstants =
new : unit -> RethinkDBConstants
static val DefaultDbName : string
static val DefaultHostname : string
static val DefaultAuthkey : string
static val DefaultPort : int
static val DefaultTimeout : int
nested type Protocol
Full name: RethinkDb.Driver.RethinkDBConstants
--------------------
RethinkDBConstants() : unit
Full name: Quatro.DataConfig.CreateConnection
member RunAsync<'T> : term:ReqlAst * globalOpts:obj * cancelToken:CancellationToken -> Task<obj>
member RunAtomAsync<'T> : term:ReqlAst * globalOpts:obj * cancelToken:CancellationToken -> Task<'T>
member RunCursorAsync<'T> : term:ReqlAst * globalOpts:obj * cancelToken:CancellationToken -> Task<Cursor<'T>>
member RunNoReply : term:ReqlAst * globalOpts:obj -> unit
member RunResultAsync<'T> : term:ReqlAst * globalOpts:obj * cancelToken:CancellationToken -> Task<'T>
Full name: RethinkDb.Driver.Net.IConnection
member CheckOpen : unit -> unit
member ClientEndPoint : IPEndPoint
member Close : ?shouldNoReplyWait:bool -> unit
member Db : string
member Dispose : unit -> unit
member HasError : bool
member Hostname : string
member NoReplyWait : unit -> unit
member NoReplyWaitAsync : ?cancelToken:CancellationToken -> Task
member Open : bool
...
nested type Builder
Full name: RethinkDb.Driver.Net.Connection
new : unit -> Builder
member AuthKey : key:string -> Builder
member Connect : unit -> Connection
member ConnectAsync : unit -> Task<Connection>
member Db : val:string -> Builder
member Hostname : val:string -> Builder
member Port : val:int -> Builder
member Timeout : val:int -> Builder
member User : user:string * password:string -> Builder
Full name: RethinkDb.Driver.Net.Connection.Builder
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.fold
type RethinkDB =
inherit TopLevel
new : unit -> RethinkDB
member Connection : unit -> Builder
member ConnectionPool : unit -> Builder
static val R : RethinkDB
Full name: RethinkDb.Driver.RethinkDB
--------------------
RethinkDB() : unit
interface
abstract member Conn : IConnection
end
Full name: Quatro.IDependencies
Full name: Quatro.IDependencies.Conn
type AutoOpenAttribute =
inherit Attribute
new : unit -> AutoOpenAttribute
new : path:string -> AutoOpenAttribute
member Path : string
Full name: Microsoft.FSharp.Core.AutoOpenAttribute
--------------------
new : unit -> AutoOpenAttribute
new : path:string -> AutoOpenAttribute
Full name: Quatro.DependencyExtraction.getConn
private new : unit -> AsyncBuilder
member Bind : computation:Async<'T> * binder:('T -> Async<'U>) -> Async<'U>
member Combine : computation1:Async<unit> * computation2:Async<'T> -> Async<'T>
member Delay : generator:(unit -> Async<'T>) -> Async<'T>
member For : sequence:seq<'T> * body:('T -> Async<unit>) -> Async<unit>
member Return : value:'T -> Async<'T>
member ReturnFrom : computation:Async<'T> -> Async<'T>
member TryFinally : computation:Async<'T> * compensation:(unit -> unit) -> Async<'T>
member TryWith : computation:Async<'T> * catchHandler:(exn -> Async<'T>) -> Async<'T>
member Using : resource:'T * binder:('T -> Async<'U>) -> Async<'U> (requires 'T :> IDisposable)
...
Full name: Microsoft.FSharp.Control.AsyncBuilder
Full name: Quatro.ExampleExtensions.Bind
An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
a standard .NET task
type Task =
new : action:Action -> Task + 7 overloads
member AsyncState : obj
member ContinueWith : continuationAction:Action<Task> -> Task + 9 overloads
member CreationOptions : TaskCreationOptions
member Dispose : unit -> unit
member Exception : AggregateException
member Id : int
member IsCanceled : bool
member IsCompleted : bool
member IsFaulted : bool
...
Full name: System.Threading.Tasks.Task
--------------------
type Task<'TResult> =
inherit Task
new : function:Func<'TResult> -> Task<'TResult> + 7 overloads
member ContinueWith : continuationAction:Action<Task<'TResult>> -> Task + 9 overloads
member Result : 'TResult with get, set
static member Factory : TaskFactory<'TResult>
Full name: System.Threading.Tasks.Task<_>
--------------------
Task(action: Action) : unit
Task(action: Action, cancellationToken: Threading.CancellationToken) : unit
Task(action: Action, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj) : unit
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : unit
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
--------------------
Task(function: Func<'TResult>) : unit
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken) : unit
Task(function: Func<'TResult>, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj) : unit
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : unit
Task(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
a standard .NET task which does not commpute a value
member AsyncBuilder.Bind : t:Task<'T> * f:('T -> Async<'R>) -> Async<'R>
An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
a standard .NET task
member AsyncBuilder.Bind : computation:Async<'T> * binder:('T -> Async<'U>) -> Async<'U>
static member Async.AwaitTask : task:Task<'T> -> Async<'T>
Full name: Quatro.ExampleExtensions.Bind
An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
a standard .NET task which does not commpute a value
Full name: Microsoft.FSharp.Core.unit
Full name: Quatro.ExampleExtensions.ReturnFrom
type RequireQualifiedAccessAttribute =
inherit Attribute
new : unit -> RequireQualifiedAccessAttribute
Full name: Microsoft.FSharp.Core.RequireQualifiedAccessAttribute
--------------------
new : unit -> RequireQualifiedAccessAttribute
namespace System.Data
--------------------
namespace Microsoft.FSharp.Data
Full name: Quatro.Data.establishEnvironment
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = String
Full name: Microsoft.FSharp.Core.string
Full name: Quatro.IConnectionExtensions.EstablishEnvironment
from Quatro
Full name: Quatro.App.lazyCfg
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
...
Full name: System.IO.File
File.ReadAllText(path: string, encoding: Text.Encoding) : string
Full name: Quatro.App.cfg
Full name: Quatro.App.deps
Full name: Quatro.App.main
module Data
from Quatro
--------------------
namespace System.Data
--------------------
namespace Microsoft.FSharp.Data