19

I am familiar with the fact that in F# there is no equivalent "return" keyword.

However we came across an issue recently where we have needed a workflow that consists of many steps, where each step can return a good or bad result. If a bad result is found in any of the steps, we wanted to exit the workflow - and exit early!

We got around it by effectively checking for an Error in each step (i.e. function) but I dont feel that this is the correct way to do it - it is inefficient and we dont exit early.

A sample function in the workflow is as follows:

let StepB stepAResult someParameters =
    match stepAResult with
    | Good(validResourceData) ->
        // Do current step processing
        // Return current step result
    | Error(error) -> error |> Result.Error

The workflow itself is like the following:

let Workflow someParameters =
    let stepAResult = StepA someParameters
    let stepBResult = StepB stepAResult someParameters
    let stepCResult = StepC stepBResult someParameters
    let stepDResult = StepD stepCResult someParameters
    stepDResult

So each sample function would take in the result of the previous function and only execute the current step if there was no Error!

The problem I have with this is that if StepA fails with an Error, every other step is still called.

Is there a "functional" way of "returning early" instead of calling every function in the workflow, where we must check each time for an Error?

bstack
  • 2,466
  • 3
  • 25
  • 38

4 Answers4

15

You write your functions under the assumption, that all went well as you have done. Then you unwrap the happy case and continue with the happy case.

And in the end, you can use the builder to make the syntax pretty.

type Result<'TSuccess, 'TError> = 
    | Success of 'TSuccess
    | Error of 'TError

type ResultBuilder() =
    member this.Bind(v, f) =
        match v with
        | Success v -> f v
        | Error e -> Error e

    member this.Return value = Success value

let result = ResultBuilder()

let bla<'a> = result {
    let! successOne = Success 1
    let! successTwo = Success 2
    let! failure = Error "after this, the computation exited"
    failwith "Boom, won't occurr"
    return successOne + successTwo }
Daniel Fabian
  • 3,828
  • 2
  • 19
  • 28
  • 1
    This suggestion is more or less what we have gone with, however it does mean that you do not "return early". Each function in the workflow must effectively "check" for an error case. Thanks for clarifying this! – bstack Apr 28 '15 at 09:28
  • One thing to keep in mind here is that all stages have to be executed since each stage can alter the success or error type and you have to have a deterministic result type at the end. – melston Apr 18 '21 at 22:48
9

The other answers are great, computation expressions would be perfect for this.

Just to provide another option, it's worth noting that there are a couple of special cases in F# code structure that allow for a less painful "return-early" story.

The canonical indent-based formatting might be this mess:

let step1 = ...
if failed step1 then
    () // bail out
else
    let step2 = ...
    if failed step2 then
        ()
    else
        let step3 = ...
        if failed step3 then
            ()
        else 
            let step4 = ...
            ...

Two alternative formattings are below. They look odd, but really are quite handy.

let step1 = ...
if failed step1 then
    () // bail out
else

let step2 = ...
if failed step2 then
    ()
else

let step3 = ...
if failed step3 then
    ()
else 

let step4 = ...
...

or

let step1 = ...
if failed step1 then () else

let step2 = ...
if failed step2 then () else

let step3 = ...
if failed step3 then () else 

let step4 = ...
...
latkin
  • 16,402
  • 1
  • 47
  • 62
3

This is what Computation Expressions are for.

Computation Expressions provide nice syntactic sugar to do what is known as monadic composition, where the previous resultant value is checked automatically before the next step is executed.

I did a talk on this concept recently - it's posted on youtube at https://www.youtube.com/watch?v=gNNTuN6wWVc; and @scottwlaschin has a detailed introduction to it at http://fsharpforfunandprofit.com/series/computation-expressions.html

Ping me on twitter if you want more help!

  • But does this mean that we have to check at every step? If step A fails with an error, I want to exit, I dont want to have to check this in step B, C and D? – bstack Apr 24 '15 at 14:52
  • 1
    What do you mean by "exit"? * If it means that you don't perform any of the subsequent functions in the workflow, that's exactly what happens in the workflow. * If it means that you don't do any further checking of the result value, it actually cannot be done in a general fashion.. The notion of "exit" is a hangover from a very imperative style of thinking - you'd be better served by using the notion of a value composed out of chaining multiple functions safely... – John Azariah Apr 24 '15 at 15:07
  • 1
    Also, you don't have to write the checking code yourself - you only provide the functions that participate in the workflow in the "success" case, and functional mechanics lift those functions to apply the checking code for you...so you don't really have to worry about code-bloat or maintenance issues you get in imperative-style code! – John Azariah Apr 24 '15 at 15:14
1

Daniel's answer is syntactic sugar to continuation style approach. Here the de-sugared version:

let step1 parm cont =
    if true then cont 42 else None
let step2 parm cont =
    if false then cont 69 else None
let conti parm =
    step1 parm (fun result1 -> 
    step2 parm (fun result2 -> 
    Some(result1 + result2)))
BitTickler
  • 10,905
  • 5
  • 32
  • 53