I am working on understanding Railway Oriented Programming (Scott Wlaschin style) in F#. In my example I want to create a pipeline which does some calculation, applies two different functions to the output of that initial calculation, then hands off a tuple of its results to a final function.
In real code the 'initial calculation' might be getting some data, the 'two different functions' might produce a textual summary and a chart of the data, and the final function might be rendering some output containing both the text and the chart.
I've invented a bind2
function to achieve this, but I can't help thinking that if this was a good idea it would already be common practice and must appear (in disguise) in existing libraries such as Scott's.
Please let me know how I would achieve what I want in an idiomatic style.
Here's my code. All above bind2
is taken broadly from Scott's code, the remainder is my bind2
function and a simple demo.
type Result<'S, 'F> =
| Success of 'S
| Failure of message : 'F list
let bind f x =
match x with
| Success s -> f s
| Failure f -> Failure f
let bind2 (f1 : 'A -> Result<'B, 'F>)
(f2 : 'A -> Result<'C, 'F>) (x : Result<'A, 'F>) : Result<'B * 'C, 'F> =
match x with
| Success _ ->
let r1, r2 = (bind f1) x, (bind f2) x
match r1, r2 with
| Success s1, Success s2 ->
Success(s1, s2)
| Failure f1, _ ->
Failure f1
| _, Failure f2 ->
Failure f2
| Failure f -> Failure f
let demo1 (a: int) : Result<int, string> =
Success a
let demo2 (a: int) : Result<string, string> =
a |> string |> Success
let demo3 (a: int) : Result<string, string> =
a |> (*) -1 |> string |> Success
let demo4 (s1 : string, s2 : string) =
sprintf "%s - %s" s1 s2 |> Success
let Demo =
bind demo1
>> bind2 demo2 demo3
>> bind demo4
3 Answers 3
I actually wouldn't bother defining a bind2 as this can lead to an explosion, bind3, 4, 5, 6. Instead I would define a simple computation builder to do this. So using your code from above.
type DemoBuilder() =
member x.Bind(m,f) = bind f m
member x.Return(s) = Success s
let demo = DemoBuilder()
which you can then use like so
let DemoB x =
demo {
let! input = x
let! a = demo1 input
let! b = demo2 a
let! c = demo3 a
return (b,c)
}
DemoB (Success 1)
-
\$\begingroup\$ I was beginning to suspect that might be the endgame of all this. Thanks! \$\endgroup\$Kit– Kit2016年09月22日 09:32:04 +00:00Commented Sep 22, 2016 at 9:32
If you are chaining a series of results and combining them in various ways, then creating a computation expression (Colin's answer) is definitely the best approach.
For fun, let's look at two other approaches :)
First, it's useful to realize that your bind2
is doing too many things at once.
You can break it into two parts: combining two results to get a tuple, and doing the bind.
That leads to defining a simpler function: combine
:
type Result<'S, 'F> = ...
let bind f x = ...
let combine (xR : Result<'B, 'F>)
(yR : Result<'C, 'F>)
: Result<'B * 'C, 'F> =
match xR,yR with
| Success x, Success y -> Success (x,y)
| Failure e, Success y -> Failure e
| Success x, Failure e -> Failure e
| Failure e1, Failure e2 -> Failure e1 // or Failure (e1 @ e2) if lists
Your bind2
can now be defined using bind
and combine
let bind2 (f1 : 'A -> Result<'B, 'F>)
(f2 : 'A -> Result<'C, 'F>)
(x : Result<'A, 'F>)
: Result<'B * 'C, 'F> =
let r1, r2 = (bind f1) x, (bind f2) x
combine r1 r2
But it's not really needed, as you can normally just do it inline:
let demo1 = ...
let demo2 = ...
let demo3 = ...
let demo4 = ...
let Demo2 x =
let y = (bind demo1) x
let r1 = (bind demo2) y
let r2 = (bind demo2) y
combine r1 r2
All those binds are ugly, which is why a computation expression is a much nicer syntax for the same thing!
combine
is an example of applicative style, which I discuss in more detail here.
You can think of the Result
type as an "elevated world", and combine
is adding two values in that world.
Note that in this approach ("applicative style") you are getting the results from both functions and combining them. That's great for things like validation, but if you want to avoid calling the second function if the first function fails ("monadic style") then the computation expression is the way to go.
Kleisli composition
Finally, if you are working only with the functions of the form 'A -> Result<'B, 'F>
, then you can also use another
approach, which is Kleisli composition. In "Kleisli world" all the values are functions of the form 'A -> Result<'B, 'F>
and you have various tools to combine them.
Here's the Kleisli version of bind
and combine
:
// compose two Kliesli functions
let composeK (f1 : 'A -> Result<'B, 'F>)
(f2 : 'B -> Result<'C, 'F>)
: 'A -> Result<'C, 'F> =
f1 >> bind f2
// common symbol for Kliesli composition
let (>=>) f1 f2 = composeK f1 f2
// Kliesli combine
let combineK (f1 : 'A -> Result<'B, 'F>)
(f2 : 'A -> Result<'C, 'F>)
: 'A -> Result<'B * 'C, 'F> =
fun a ->
let r1 = f1 a
let r2 = f2 a
combine r1 r2
And with them, you can write the Demo
function as:
let Demo3 =
demo1 >=> combineK demo2 demo3
Personally, I would stay away from this though, as it makes my head hurt trying to understand what's going on!
-
\$\begingroup\$ But Colin's answer is the correct one! You should choose that :) \$\endgroup\$Grundoon– Grundoon2016年09月22日 10:20:42 +00:00Commented Sep 22, 2016 at 10:20
Just a note: you should not evaluate (bind f2) until result of (bind f1) is known.