F# for Fun and Profit
  • Introduction
  • Getting started
    • Contents of the book
    • "Why use F#?" in one page
    • Installing and using F#
    • F# syntax in 60 seconds
    • Learning F#
    • Troubleshooting F#
    • Low-risk ways to use F# at work
      • Twenty six low-risk ways to use F# at work
      • Using F# for development and devops scripts
      • Using F# for testing
      • Using F# for database related tasks
      • Other interesting ways of using F# at work
  • Why use F#?
    • The "Why use F#?" Series
      • Introduction to the 'Why use F#' series
      • F# syntax in 60 seconds
      • Comparing F# with C#: A simple sum
      • Comparing F# with C#: Sorting
      • Comparing F# with C#: Downloading a web page
      • Four Key Concepts
      • Conciseness
      • Type inference
      • Low overhead type definitions
      • Using functions to extract boilerplate code
      • Using functions as building blocks
      • Pattern matching for conciseness
      • Convenience
      • Out-of-the-box behavior for types
      • Functions as interfaces
      • Partial Application
      • Active patterns
      • Correctness
      • Immutability
      • Exhaustive pattern matching
      • Using the type system to ensure correct code
      • Worked example: Designing for correctness
      • Concurrency
      • Asynchronous programming
      • Messages and Agents
      • Functional Reactive Programming
      • Completeness
      • Seamless interoperation with .NET libraries
      • Anything C# can do...
      • Why use F#: Conclusion
  • Thinking Functionally
    • The "Thinking Functionally" Series
      • Thinking Functionally: Introduction
      • Mathematical functions
      • Function Values and Simple Values
      • How types work with functions
      • Currying
      • Partial application
      • Function associativity and composition
      • Defining functions
      • Function signatures
      • Organizing functions
      • Attaching functions to types
      • Worked example: A stack based calculator
  • Understanding F# ###
    • The "Expressions and syntax" Series
      • Expressions and syntax: Introduction
      • Expressions vs. statements
      • Overview of F# expressions
      • Binding with let, use, and do
      • F# syntax: indentation and verbosity
      • Parameter and value naming conventions
      • Control flow expressions
      • Exceptions
      • Match expressions
      • Formatted text using printf
      • Worked example: Parsing command line arguments
      • Worked example: Roman numerals
    • The "Understanding F# types" Series
      • Understanding F# types: Introduction
      • Overview of types in F#
      • Type abbreviations
      • Tuples
      • Records
      • Discriminated Unions
      • The Option type
      • Enum types
      • Built-in .NET types
      • Units of measure
      • Understanding type inference
    • Choosing between collection functions
    • The "Object-oriented programming in F#" Series
      • Object-oriented programming in F#: Introduction
      • Classes
      • Inheritance and abstract classes
      • Interfaces
      • Object expressions
    • The "Computation Expressions" Series
      • Computation expressions: Introduction
      • Understanding continuations
      • Introducing 'bind'
      • Computation expressions and wrapper types
      • More on wrapper types
      • Implementing a builder: Zero and Yield
      • Implementing a builder: Combine
      • Implementing a builder: Delay and Run
      • Implementing a builder: Overloading
      • Implementing a builder: Adding laziness
      • Implementing a builder: The rest of the standard methods
    • Organizing modules in a project
    • The "Dependency cycles" Series
      • Cyclic dependencies are evil
      • Refactoring to remove cyclic dependencies
      • Cycles and modularity in the wild
    • The "Porting from C#" Series
      • Porting from C# to F#: Introduction
      • Getting started with direct porting
  • Functional Design ###
    • The "Designing with types" Series
      • Designing with types: Introduction
      • Single case union types
      • Making illegal states unrepresentable
      • Discovering new concepts
      • Making state explicit
      • Constrained strings
      • Non-string types
      • Designing with types: Conclusion
    • Algebraic type sizes and domain modelling
    • Thirteen ways of looking at a turtle
      • Thirteen ways of looking at a turtle (part 2)
      • Thirteen ways of looking at a turtle - addendum
  • Functional Patterns ###
    • How to design and code a complete program
    • A functional approach to error handling (Railway oriented programming)
      • Railway oriented programming: Carbonated edition
    • The "Understanding monoids" Series
      • Monoids without tears
      • Monoids in practice
      • Working with non-monoids
    • The "Understanding Parser Combinators" Series
      • Understanding Parser Combinators
      • Building a useful set of parser combinators
      • Improving the parser library
      • Writing a JSON parser from scratch
    • The "Handling State" Series
      • Dr Frankenfunctor and the Monadster
      • Completing the body of the Monadster
      • Refactoring the Monadster
    • The "Map and Bind and Apply, Oh my!" Series
      • Understanding map and apply
      • Understanding bind
      • Using the core functions in practice
      • Understanding traverse and sequence
      • Using map, apply, bind and sequence in practice
      • Reinventing the Reader monad
      • Map and Bind and Apply, a summary
    • The "Recursive types and folds" Series
      • Introduction to recursive types
      • Catamorphism examples
      • Introducing Folds
      • Understanding Folds
      • Generic recursive types
      • Trees in the real world
    • The "A functional approach to authorization" Series
      • A functional approach to authorization
      • Constraining capabilities based on identity and role
      • Using types as access tokens
  • Testing
    • An introduction to property-based testing
    • Choosing properties for property-based testing
  • Examples and Walkthroughs
    • Worked example: Designing for correctness
    • Worked example: A stack based calculator
    • Worked example: Parsing command line arguments
    • Worked example: Roman numerals
    • Commentary on 'Roman Numerals Kata with Commentary'
    • Calculator Walkthrough: Part 1
      • Calculator Walkthrough: Part 2
      • Calculator Walkthrough: Part 3
      • Calculator Walkthrough: Part 4
    • Enterprise Tic-Tac-Toe
      • Enterprise Tic-Tac-Toe, part 2
    • Writing a JSON parser from scratch
  • Other
    • Ten reasons not to use a statically typed functional programming language
    • Why I won't be writing a monad tutorial
    • Is your programming language unreasonable?
    • We don't need no stinking UML diagrams
    • Introvert and extrovert programming languages
    • Swapping type-safety for high performance using compiler directives
Powered by GitBook
On this page
  • The problem
  • Wrapping the inner type in a delay
  • Reviewing the builder class
  • True laziness
  • Summary: Immediate vs. Delayed vs. Lazy

Was this helpful?

  1. Understanding F# ###
  2. The "Computation Expressions" Series

Implementing a builder: Adding laziness

Delaying a workflow externally

PreviousImplementing a builder: OverloadingNextImplementing a builder: The rest of the standard methods

Last updated 5 years ago

Was this helpful?

In a , we saw how to avoid unnecessary evaluation of expressions in a workflow until needed.

But that approach was designed for expressions inside a workflow. What happens if we want to delay the whole workflow itself until needed.

The problem

Here is the code from our "maybe" builder class. This code is based on the trace builder from the earlier post, but with all the tracing taken out, so that it is nice and clean.

type MaybeBuilder() =

    member this.Bind(m, f) = 
        Option.bind f m

    member this.Return(x) = 
        Some x

    member this.ReturnFrom(x) = 
        x

    member this.Zero() = 
        None

    member this.Combine (a,b) = 
        match a with
        | Some _ -> a  // if a is good, skip b
        | None -> b()  // if a is bad, run b

    member this.Delay(f) = 
        f

    member this.Run(f) = 
        f()

// make an instance of the workflow                
let maybe = new MaybeBuilder()

Before moving on, make sure that you understand how this works. If we analyze this using the terminology of the earlier post, we can see that the types used are:

  • Wrapper type: 'a option

  • Internal type: 'a option

  • Delayed type: unit -> 'a option

Now let's check this code and make sure everything works as expected.

maybe { 
    printfn "Part 1: about to return 1"
    return 1
    printfn "Part 2: after return has happened"
    } |> printfn "Result for Part1 but not Part2: %A" 

// result - second part is NOT evaluated    

maybe { 
    printfn "Part 1: about to return None"
    return! None
    printfn "Part 2: after None, keep going"
    } |> printfn "Result for Part1 and then Part2: %A" 

// result - second part IS evaluated

But what happens if we refactor the code into a child workflow, like this:

let childWorkflow = 
    maybe {printfn "Child workflow"} 

maybe { 
    printfn "Part 1: about to return 1"
    return 1
    return! childWorkflow 
    } |> printfn "Result for Part1 but not childWorkflow: %A"

The output shows that the child workflow was evaluated even though it wasn't needed in the end. This might not be a problem in this case, but in many cases, we may not want this to happen.

So, how to avoid it?

Wrapping the inner type in a delay

The obvious approach is to wrap the entire result of the builder in a delay function, and then to "run" the result, we just evaluate the delay function.

So, here's our new wrapper type:

type Maybe<'a> = Maybe of (unit -> 'a option)

And now we need to change the Run method as well. Previously, it evaluated the delay function that was passed in to it, but now it should leave it unevaluated and wrap it in our new wrapper type:

// before
member this.Run(f) = 
    f()

// after    
member this.Run(f) = 
    Maybe f

I've forgotten to fix up another method -- do you know which one? We'll bump into it soon!

One more thing -- we'll need a way to "run" the result now.

let run (Maybe f) = f()

Let's try out our new type on our previous examples:

let m1 = maybe { 
    printfn "Part 1: about to return 1"
    return 1
    printfn "Part 2: after return has happened"
    }

Running this, we get something like this:

val m1 : Maybe<int> = Maybe <fun:m1@123-7>

That looks good; nothing else was printed.

And now run it:

run m1 |> printfn "Result for Part1 but not Part2: %A"

and we get the output:

Part 1: about to return 1
Result for Part1 but not Part2: Some 1

Perfect. Part 2 did not run.

But we run into a problem with the next example:

let m2 = maybe { 
    printfn "Part 1: about to return None"
    return! None
    printfn "Part 2: after None, keep going"
    }

Oops! We forgot to fix up ReturnFrom! As we know, that method takes a wrapped type, and we have redefined the wrapped type now.

Here's the fix:

member this.ReturnFrom(Maybe f) = 
    f()

We are going to accept a Maybe from outside, and then immediately run it to get at the option.

But now we have another problem -- we can't return an explicit None anymore in return! None, we have to return a Maybe type instead. How are we going to create one of these?

Well, we could create a helper function that constructs one for us. But there is a much simpler answer: you can create a new Maybe type by using a maybe expression!

let m2 = maybe { 
    return! maybe {printfn "Part 1: about to return None"}
    printfn "Part 2: after None, keep going"
    }

This is why the Zero method is useful. With Zero and the builder instance, you can create new instances of the type even if they don't do anything.

But now we have one more error -- the dreaded "value restriction":

Value restriction. The value 'm2' has been inferred to have generic type

The reason why this has happened is that both expressions are returning None. But the compiler does not know what type None is. The code is using None of type Option<obj> (presumably because of implicit boxing) yet the compiler knows that the type can be more generic than that.

There are two fixes. One is to make the type explicit:

let m2_int: Maybe<int> = maybe { 
    return! maybe {printfn "Part 1: about to return None"}
    printfn "Part 2: after None, keep going;"
    }

Or we can just return some non-None value instead:

let m2 = maybe { 
    return! maybe {printfn "Part 1: about to return None"}
    printfn "Part 2: after None, keep going;"
    return 1
    }

Both of these solutions will fix the problem.

Now if we run the example, we see that the result is as expected. The second part is run this time.

run m2 |> printfn "Result for Part1 and then Part2: %A"

The trace output:

Part 1: about to return None
Part 2: after None, keep going;
Result for Part1 and then Part2: Some 1

Finally, we'll try the child workflow examples again:

let childWorkflow = 
    maybe {printfn "Child workflow"} 

let m3 = maybe { 
    printfn "Part 1: about to return 1"
    return 1
    return! childWorkflow 
    } 

run m3 |> printfn "Result for Part1 but not childWorkflow: %A"

And now the child workflow is not evaluated, just as we wanted.

And if we do need the child workflow to be evaluated, this works too:

let m4 = maybe { 
    return! maybe {printfn "Part 1: about to return None"}
    return! childWorkflow 
    } 

run m4 |> printfn "Result for Part1 and then childWorkflow: %A"

Reviewing the builder class

Let's look at all the code in the new builder class again:

type Maybe<'a> = Maybe of (unit -> 'a option)

type MaybeBuilder() =

    member this.Bind(m, f) = 
        Option.bind f m

    member this.Return(x) = 
        Some x

    member this.ReturnFrom(Maybe f) = 
        f()

    member this.Zero() = 
        None

    member this.Combine (a,b) = 
        match a with
        | Some _' -> a    // if a is good, skip b
        | None -> b()     // if a is bad, run b

    member this.Delay(f) = 
        f

    member this.Run(f) = 
        Maybe f

// make an instance of the workflow                
let maybe = new MaybeBuilder()

let run (Maybe f) = f()

If we analyze this new builder using the terminology of the earlier post, we can see that the types used are:

  • Wrapper type: Maybe<'a>

  • Internal type: 'a option

  • Delayed type: unit -> 'a option

Note that in this case it was convenient to use the standard 'a option as the internal type, because we didn't need to modify Bind or Return at all.

An alternative design might use Maybe<'a> as the internal type as well, which would make things more consistent, but makes the code harder to read.

True laziness

Let's look at a variant of the last example:

let child_twice: Maybe<unit> = maybe { 
    let workflow = maybe {printfn "Child workflow"} 

    return! maybe {printfn "Part 1: about to return None"}
    return! workflow 
    return! workflow 
    } 

run child_twice |> printfn "Result for childWorkflow twice: %A"

What should happen? How many times should the child workflow be run?

The delayed implementation above does ensure that the child workflow is only be evaluated on demand, but it does not stop it being run twice.

In some situations, you might require that the workflow is guaranteed to only run at most once, and then cached ("memoized"). This is easy enough to do using the Lazy type that is built into F#.

The changes we need to make are:

  • Change Maybe to wrap a Lazy instead of a delay

  • Change ReturnFrom and run to force the evaluation of the lazy value

  • Change Run to run the delay from inside a lazy

Here is the new class with the changes:

type Maybe<'a> = Maybe of Lazy<'a option>

type MaybeBuilder() =

    member this.Bind(m, f) = 
        Option.bind f m

    member this.Return(x) = 
        Some x

    member this.ReturnFrom(Maybe f) = 
        f.Force()

    member this.Zero() = 
        None

    member this.Combine (a,b) = 
        match a with
        | Some _' -> a    // if a is good, skip b
        | None -> b()     // if a is bad, run b

    member this.Delay(f) = 
        f

    member this.Run(f) = 
        Maybe (lazy f())

// make an instance of the workflow                
let maybe = new MaybeBuilder()

let run (Maybe f) = f.Force()

And if we run the "child twice` code from above, we get:

Part 1: about to return None
Child workflow
Result for childWorkflow twice: <null>

from which it is clear that the child workflow only ran once.

Summary: Immediate vs. Delayed vs. Lazy

On this page, we've seen three different implementations of the maybe workflow. One that is always evaluated immediately, one that uses a delay function, and one that uses laziness with memoization.

So... which approach should you use?

There is no single "right" answer. Your choice depends on a number of things:

  • Is the code in the expression cheap to execute, and without important side-effects? If so, stick with the first, immediate version. It's simple and easy to understand, and this is exactly what most implementations of the maybe workflow use.

  • Is the code in the expression expensive to execute, might the result vary with each call (e.g. non-deterministic), or are there important side-effects? If so, use the second, delayed version. This is exactly what most other workflows do, especially those relating to I/O (such as async).

  • F# does not attempt to be a purely functional language, so almost all F# code will fall into one of these two categories. But, if you need to code in a guaranteed side-effect free style, or you just want to ensure that expensive code is evaluated at most once, then use the third, lazy option.

Whatever your choice, do make it clear in the documentation. For example, the delayed vs. lazy implementations appear exactly the same to the client, but they have very different semantics, and the client code must be written differently for each case.

Now that we have finished with delays and laziness, we can go back to the builder methods and finish them off.

We've replaced a simple option with a function that evaluates to an option, and then wrapped that function in a for good measure.

previous post
single case union