functional-programmingf#monads

Why does `let fmap f = id >=> (Ok << f)` work?


Asked How to implement map using the fish (>=>, Kleisli composition) operator in F#? a couple of hours ago, and 'kaefer's answer blew my mind:

let fmap f = id >=> (Ok << f)

It is perfect in its simplicity, but when I look at the type signatures, it is not supposed to work. I've been at it for almost an hour now, so I'm probably missing something fundamental how F# evaluates expressions...

For the record, these are the implementations of bind,switch, and >=>:

let bind
    (     f : 'a -> Result<'b,'c>)
    (result :       Result<'a,'c>)
    =
    match result with 
    |    Ok o -> f o
    | Error e -> Error e

let (>=>)
    (f : 'a -> Result<'b,'error>)
    (g : 'b -> Result<'c,'error>)
    =
    f >> (bind g)

My next attempt was using an alternative implementation for >=>:

let (>=>>)
    (f : 'a -> Result<'b,'error>)
    (g : 'b -> Result<'c,'error>)
    x
    =
    match (f x) with
    |    Ok o -> g o
    | Error e -> Error e

Here's the test invocation on dotnet fsi:

(id >=>> (Ok << ((+) 2) : int -> Result<int,string>))
((Ok 27) : Result<int,string>)
//=> Ok 29

I'm already stuck at why >=>> does not blow up on id as its first argument?

This is how I would think evaluation goes, but apparently this is not it:

id >=>> switch ((+) 2)
          |
          V
(>=>>) id (switch ((+) 2))
          |
          V
    match (id x) with
    |    Ok o -> (Ok << ((+) 2)) o
    | Error e -> Error e

Note to future self:

(>=>>)                           (>=>>)
  (f : 'a -> Result<'b,'error>)    id
  (g : 'b -> Result<'c,'error>)    (Ok << ((+) 2) : int -> Result<int,string>))
  x                                ((Ok 27) : Result<int,string>)

(... and make sure to convert a point-free function if it does not make sense at first.)


Solution

  • Since the expression let fmap f = id >=> (Ok << f) has parentheses, we need to evaluate the inner expression first. That's the Ok << f part.

    What does Ok << f mean? You can ask FSI:

    > Ok;;
    val it: ResultValue: 'a -> Result<'a,'b>
    

    So, that's a function that takes an 'a and returns a Result<'a,'b>.

    OK, what about Ok << f? You can't just ask FSI about that, because in isolation f isn't defined, but you can ask FSI what a lambda expression that takes f would look like:

    > fun f -> Ok << f;;
    val it: f: ('a -> 'b) -> ('a -> Result<'b,'c>)
    

    From this we learn that FSI and the F# compiler infers that f must be a function that takes 'a and returns 'b. This makes sense, since it's the most general interpretation of the Ok << f expression: You take any function f and compose its output with the Ok function, and you get an 'a -> Result<'b,'c> function as return value.

    Finally, then, how about id >=> (Ok << f)?

    The Klesli operator >=> takes two 'lift'-like functions and combines them. We already know what the expression on the right-hand side looks like:

    ('a -> Result<'b,'c>)
    

    Thus, the left-hand side must be some expression that returns Result<'a,'c>: ?? -> Result<'a,'c>.

    And since id returns its input ('T -> 'T) it can only be inferred to have the type

    Result<'a,'c> -> Result<'a,'c>
    

    So it all type-checks. It's a very roundabout way to implement fmap, but it works.