I'm trying to understand how the Reader monad is used in this blog post as a mechanism to perform functional dependency injection.
The part that confuses me is this bit (taken from the blog post):
Eventually, with this type in hand our simple request handler will be looking like that:
let changePass req = effect {
let! user = Db.fetchUser req.UserId
if user.Hash = bcrypt user.Salt req.OldPass then
let! salt = Random.bytes 32
do! Db.updateUser { user with Salt = salt; Hash = bcrypt salt req.NewPass }
do! Log.info "Changed password for user %i" user.Id
return Ok ()
else
do! Log.error "Password change unauthorized: user %i" user.Id
return Error "Old password is invalid"
}
As you see, there's no more env parameter being passed around. It's now an implicit part of our effect expression
The Effect type is defined as:
[<Struct>] type Effect<'env, 'out> = Effect of ('env -> 'out)
What confuses me is that the implementation of the Reader monad is the same as the one given here (F# for fun and profit) but Scott's example is very clear when it comes to reaching out to environment from within the computation expression (syntactic sugar for monadic operations in f#), while the code above does not make use of ask
at all. See Scott's code:
let readFromConsole() =
reader {
let! (console:IConsole) = Reader.ask
console.WriteLn "Enter the first value"
let str1 = console.ReadLn()
console.WriteLn "Enter the second value"
let str2 = console.ReadLn()
return str1,str2
}
How could one write a function so that the call to Reader monad's ask
function becomes implicit instead of explicit? I could not piece it together from the snippets provided in the first blog post. The accepted answer to this question is also clear about Reader being used with the ask
function.
So is it possible to make the use of ask
implicit?
In the first example, the computation is calling all the effectful primitives such as Log.info
or Db.updateUser
or Random.bytes
using either let!
(when it needs to get some result) or using do!
(when it is an action that does not return). This means that these operations, in turn, also have access to the reader monad and can access the state passed through it. They may do that using ask
or by directly accessing the reader monad state (remember that reader is just a function that takes the state and returns some value).
I only skimmed the first blog post, but you could implement e.g. Log.info
using something like:
module Log =
let info msg = reader {
let! (console:IConsole) = Reader.ask
console.WriteLn msg
}
This is accessing console through the reader monad, but when you call it, you call it just by calling do! Log.info "Hi there"
. You could also implement this without the computation expression syntax. If the reader monad was defined as:
type Reader<'T> = Reader of (Env -> 'T)
Then the above could be just:
module Log =
let info msg = Reader(fun env ->
let (console:IConsole) = Reader.ask env
console.WriteLn msg
)