Search code examples
f#monadsreader-monad

Reader Monad without explicit call to ask: is it possible?


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?


Solution

  • 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
      )