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