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
  • First implementation
  • Dependency injection of services
  • Implementation: handling digits
  • Implementation: handling Clear and Equals
  • Bumping into bind
  • Using bind in practice
  • Displaying errors
  • Using a "maybe" computation expression instead of bind
  • Implementation: handling math operations
  • Implementation: review
  • Summary

Was this helpful?

  1. Examples and Walkthroughs
  2. Calculator Walkthrough: Part 1

Calculator Walkthrough: Part 2

Testing the design with a trial implementation

PreviousCalculator Walkthrough: Part 1NextCalculator Walkthrough: Part 3

Last updated 5 years ago

Was this helpful?

In this post, I'll continue developing a simple pocket calculator app, like this:

In the , we completed a first draft of the design, using only types (no UML diagrams!).

Now it's time to create a trial implementation that uses the design.

Doing some real coding at this point acts as a reality check. It ensures that the domain model actually makes sense and is not too abstract. And of course, it often drives more questions about the requirements and domain model.

First implementation

So let's try implementing the main calculator function, and see how we do.

First, we can immediately create a skeleton that matches each kind of input and processes it accordingly.

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
        | Digit d ->
            let newState = // do something
            newState //return
        | Op op ->
            let newState = // do something
            newState //return
        | Action Clear ->
            let newState = // do something
            newState //return
        | Action Equals ->
            let newState = // do something
            newState //return

You can see that this skeleton has a case for each type of input to handle it appropriately. Note that in all cases, a new state is returned.

This style of writing a function might look strange though. Let's look at it a bit more closely.

First, we can see that createCalculate is the not the calculator function itself, but a function that returns another function. The returned function is a value of type Calculate -- that's what the :Calculate at the end means.

Here's just the top part:

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
            // code

Since it is returning a function, I chose to write it using a lambda. That's what the fun (input,state) -> is for.

But I could have also written it using an inner function, like this

let createCalculate (services:CalculatorServices) :Calculate = 
    let innerCalculate (input,state) = 
        match input with
            // code
    innerCalculate // return the inner function

Both approaches are basically the same* -- take your pick!

* Although there might be some performance differences.

Dependency injection of services

But createCalculate doesn't just return a function, it also has a services parameter. This parameter is used for doing the "dependency injection" of the services.

That is, the services are only used in createCalculate itself, and are not visible in the function of type Calculate that is returned.

The "main" or "bootstrapper" code that assembles all the components for the application would look something like this:

// create the services
let services = CalculatorServices.createServices()

// inject the services into the "factory" method
let calculate = CalculatorImplementation.createCalculate services

// the returned "calculate" function is of type Calculate 
// and can be passed into the UI, for example

// create the UI and run it
let form = new CalculatorUI.CalculatorForm(calculate)
form.Show()

Implementation: handling digits

Now let's start implementing the various parts of the calculation function. We'll start with the digits handling logic.

To keep the main function clean, let's pass the reponsibility for all the work to a helper function updateDisplayFromDigit, like this:

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
        | Digit d ->
            let newState = updateDisplayFromDigit services d state
            newState //return

Note that I'm creating a newState value from the result of updateDisplayFromDigit and then returning it as a separate step.

I could have done the same thing in one step, without an explicit newState value, as shown below:

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
        | Digit d ->
            updateDisplayFromDigit services d state

Neither approach is automatically best. I would pick one or the other depending on the context.

For simple cases, I would avoid the extra line as being unnecessary, but sometimes having an explicit return value is more readable. The name of the value tells you an indication of the return type, and it gives you something to watch in the debugger, if you need to.

Alright, let's implement updateDisplayFromDigit now. It's pretty straightforward.

  • first use the updateDisplayFromDigit in the services to actually update the display

  • then create a new state from the new display and return it.

let updateDisplayFromDigit services digit state =
    let newDisplay = services.updateDisplayFromDigit (digit,state.display)
    let newState = {state with display=newDisplay}
    newState //return

Implementation: handling Clear and Equals

Before we move onto the implementation of the math operations, lets look at handling Clear and Equals, as they are simpler.

For Clear, just init the state, using the provided initState service.

For Equals, we check if there is a pending math op. If there is, run it and update the display, otherwise do nothing. We'll put that logic in a helper function called updateDisplayFromPendingOp.

So here's what createCalculate looks like now:

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
        | Digit d -> // as above
        | Op op -> // to do
        | Action Clear ->
            let newState = services.initState()
            newState //return
        | Action Equals ->
            let newState = updateDisplayFromPendingOp services state
            newState //return

Now to updateDisplayFromPendingOp. I spent a few minutes thinking about, and I've come up with the following algorithm for updating the display:

  • First, check if there is any pending op. If not, then do nothing.

  • Next, try to get the current number from the display. If you can't, then do nothing.

  • Next, run the op with the pending number and the current number from the display. If you get an error, then do nothing.

  • Finally, update the display with the result and return a new state.

  • The new state also has the pending op set to None, as it has been processed.

And here's what that logic looks like in imperative style code:

// First version of updateDisplayFromPendingOp 
// * very imperative and ugly
let updateDisplayFromPendingOp services state =
    if state.pendingOp.IsSome then
        let op,pendingNumber = state.pendingOp.Value
        let currentNumberOpt = services.getDisplayNumber state.display
        if currentNumberOpt.IsSome then
            let currentNumber = currentNumberOpt.Value 
            let result = services.doMathOperation (op,pendingNumber,currentNumber)
            match result with
            | Success resultNumber ->
                let newDisplay = services.setDisplayNumber resultNumber 
                let newState = {display=newDisplay; pendingOp=None}
                newState //return
            | Failure error -> 
                state // original state is untouched
        else
            state // original state is untouched
    else
        state // original state is untouched

Ewww! Don't try that at home!

That code does follow the algorithm exactly, but is really ugly and also error prone (using .Value on an option is a code smell).

On the plus side, we did make extensive use of our "services", which has isolated us from the actual implementation details.

So, how can we rewrite it to be more functional?

Bumping into bind

In order to use the bind pattern effectively, it's a good idea to break the code into many small chunks.

First, the code if state.pendingOp.IsSome then do something can be replaced by Option.bind.

let updateDisplayFromPendingOp services state =
    let result =
        state.pendingOp
        |> Option.bind ???

But remember that the function has to return a state. If the overall result of the bind is None, then we have not created a new state, and we must return the original state that was passed in.

This can be done with the built-in defaultArg function which, when applied to an option, returns the option's value if present, or the second parameter if None.

let updateDisplayFromPendingOp services state =
    let result =
        state.pendingOp
        |> Option.bind ???
    defaultArg result state

You can also tidy this up a bit as well by piping the result directly into defaultArg, like this:

let updateDisplayFromPendingOp services state =
    state.pendingOp
    |> Option.bind ???
    |> defaultArg <| state

I admit that the reverse pipe for state looks strange -- it's definitely an acquired taste!

Onwards! Now what about the parameter to bind? When this is called, we know that pendingOp is present, so we can write a lambda with those parameters, like this:

let result = 
    state.pendingOp
    |> Option.bind (fun (op,pendingNumber) ->
        let currentNumberOpt = services.getDisplayNumber state.display
        // code
        )

Alternatively, we could create a local helper function instead, and connect it to the bind, like this:

let executeOp (op,pendingNumber) = 
    let currentNumberOpt = services.getDisplayNumber state.display
    /// etc

let result = 
    state.pendingOp
    |> Option.bind executeOp

I myself generally prefer the second approach when the logic is complicated, as it allows a chain of binds to be simple. That is, I try to make my code look like:

let doSomething input = return an output option
let doSomethingElse input = return an output option
let doAThirdThing input = return an output option

state.pendingOp
|> Option.bind doSomething
|> Option.bind doSomethingElse
|> Option.bind doAThirdThing

Note that in this approach, each helper function has a non-option for input but always must output an option.

Using bind in practice

Once we have the pending op, the next step is to get the current number from the display so we can do the addition (or whatever).

Rather than having a lot of logic, I'm going keep the helper function (getCurrentNumber) simple.

  • The input is the pair (op,pendingNumber)

  • The output is the triple (op,pendingNumber,currentNumber) if currentNumber is Some, otherwise None.

In other words, the signature of getCurrentNumber will be pair -> triple option, so we can be sure that is usable with the Option.bind function.

How to convert the pair into the triple? This can be done just by using Option.map to convert the currentNumber option to a triple option. If the currentNumber is Some, then the output of the map is Some triple. On the other hand, if the currentNumber is None, then the output of the map is None also.

let getCurrentNumber (op,pendingNumber) = 
    let currentNumberOpt = services.getDisplayNumber state.display
    currentNumberOpt 
    |> Option.map (fun currentNumber -> (op,pendingNumber,currentNumber))

let result = 
    state.pendingOp
    |> Option.bind getCurrentNumber
    |> Option.bind ???

We can rewrite getCurrentNumber to be a bit more idiomatic by using pipes:

let getCurrentNumber (op,pendingNumber) = 
    state.display
    |> services.getDisplayNumber 
    |> Option.map (fun currentNumber -> (op,pendingNumber,currentNumber))

Now that we have a triple with valid values, we have everything we need to write a helper function for the math operation.

  • It takes a triple as input (the output of getCurrentNumber)

  • It does the math operation

  • It then pattern matches the Success/Failure result and outputs the new state if applicable.

let doMathOp (op,pendingNumber,currentNumber) = 
    let result = services.doMathOperation (op,pendingNumber,currentNumber)
    match result with
    | Success resultNumber ->
        let newDisplay = services.setDisplayNumber resultNumber 
        let newState = {display=newDisplay; pendingOp=None}
        Some newState //return something
    | Failure error -> 
        None // failed

Note that, unlike the earlier version with nested ifs, this version returns Some on success and None on failure.

Displaying errors

Writing the code for the Failure case made me realize something. If there is a failure, we are not displaying it at all, just leaving the display alone. Shouldn't we show an error or something?

Hey, we just found a requirement that got overlooked! This is why I like to create an implementation of the design as soon as possible. Writing real code that deals with all the cases will invariably trigger a few "what happens in this case?" moments.

So how are we going to implement this new requirement?

In order to do this, we'll need a new "service" that accepts a MathOperationError and generates a CalculatorDisplay.

type SetDisplayError = MathOperationError -> CalculatorDisplay

and we'll need to add it to the CalculatorServices structure too:

type CalculatorServices = {
    // as before
    setDisplayNumber: SetDisplayNumber 
    setDisplayError: SetDisplayError 
    initState: InitState 
    }

doMathOp can now be altered to use the new service. Both Success and Failure cases now result in a new display, which in turn is wrapped in a new state.

let doMathOp (op,pendingNumber,currentNumber) = 
    let result = services.doMathOperation (op,pendingNumber,currentNumber)
    let newDisplay = 
        match result with
        | Success resultNumber ->
            services.setDisplayNumber resultNumber 
        | Failure error -> 
            services.setDisplayError error
    let newState = {display=newDisplay;pendingOp=None}
    Some newState //return something

I'm going to leave the Some in the result, so we can stay with Option.bind in the result pipeline*.

* An alternative would be to not return Some, and then use Option.map in the result pipeline

Putting it all together, we have the final version of updateDisplayFromPendingOp. Note that I've also added a ifNone helper that makes defaultArg better for piping.

// helper to make defaultArg better for piping
let ifNone defaultValue input = 
    // just reverse the parameters!
    defaultArg input defaultValue 

// Third version of updateDisplayFromPendingOp 
// * Updated to show errors on display in Failure case
// * replaces awkward defaultArg syntax
let updateDisplayFromPendingOp services state =
    // helper to extract CurrentNumber
    let getCurrentNumber (op,pendingNumber) = 
        state.display
        |> services.getDisplayNumber 
        |> Option.map (fun currentNumber -> (op,pendingNumber,currentNumber))

    // helper to do the math op
    let doMathOp (op,pendingNumber,currentNumber) = 
        let result = services.doMathOperation (op,pendingNumber,currentNumber)
        let newDisplay = 
            match result with
            | Success resultNumber ->
                services.setDisplayNumber resultNumber 
            | Failure error -> 
                services.setDisplayError error
        let newState = {display=newDisplay;pendingOp=None}
        Some newState //return something

    // connect all the helpers
    state.pendingOp
    |> Option.bind getCurrentNumber
    |> Option.bind doMathOp 
    |> ifNone state // return original state if anything fails

Using a "maybe" computation expression instead of bind

So far, we've being using "bind" directly. That has helped by removing the cascading if/else.

Since we are dealing with Options, we can create a "maybe" computation expression that allows clean handling of options. (If we were dealing with other types, we would need to create a different computation expression for each type).

Here's the definition -- only four lines!

type MaybeBuilder() =
    member this.Bind(x, f) = Option.bind f x
    member this.Return(x) = Some x

let maybe = new MaybeBuilder()

With this computation expression available, we can use maybe instead of bind, and our code would look something like this:

let doSomething input = return an output option
let doSomethingElse input = return an output option
let doAThirdThing input = return an output option

let finalResult = maybe {
    let! result1 = doSomething
    let! result2 = doSomethingElse result1
    let! result3 = doAThirdThing result2
    return result3
    }

In our case, then we can write yet another version of updateDisplayFromPendingOp -- our fourth!

// Fourth version of updateDisplayFromPendingOp 
// * Changed to use "maybe" computation expression
let updateDisplayFromPendingOp services state =

    // helper to do the math op
    let doMathOp (op,pendingNumber,currentNumber) = 
        let result = services.doMathOperation (op,pendingNumber,currentNumber)
        let newDisplay = 
            match result with
            | Success resultNumber ->
                services.setDisplayNumber resultNumber 
            | Failure error -> 
                services.setDisplayError error
        {display=newDisplay;pendingOp=None}

    // fetch the two options and combine them
    let newState = maybe {
        let! (op,pendingNumber) = state.pendingOp
        let! currentNumber = services.getDisplayNumber state.display
        return doMathOp (op,pendingNumber,currentNumber)
        }
    newState |> ifNone state

Note that in this implementation, I don't need the getCurrentNumber helper any more, as I can just call services.getDisplayNumber directly.

So, which of these variants do I prefer?

It depends.

  • On the other hand, if I am pulling options from many different places, and I want to combine them in various ways, the maybe computation expression makes it easier.

So, in this case, I'll go for the last implementation, using maybe.

Implementation: handling math operations

Now we are ready to do the implementation of the math operation case.

First, if there is a pending operation, the result will be shown on the display, just as for the Equals case. But in addition, we need to push the new pending operation onto the state as well.

For the math operation case, then, there will be two state transformations, and createCalculate will look like this:

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
        | Digit d -> // as above
        | Op op ->
            let newState1 = updateDisplayFromPendingOp services state
            let newState2 = addPendingMathOp services op newState1 
            newState2 //return

We've already defined updateDisplayFromPendingOp above. So we just need addPendingMathOp as a helper function to push the operation onto the state.

The algorithm for addPendingMathOp is:

  • Try to get the current number from the display. If you can't, then do nothing.

  • Update the state with the op and current number.

Here's the ugly version:

// First version of addPendingMathOp 
// * very imperative and ugly
let addPendingMathOp services op state = 
    let currentNumberOpt = services.getDisplayNumber state.display
    if currentNumberOpt.IsSome then 
        let currentNumber = currentNumberOpt.Value 
        let pendingOp = Some (op,currentNumber)
        let newState = {state with pendingOp=pendingOp}
        newState //return
    else                
        state // original state is untouched

Again, we can make this more functional using exactly the same techniques we used for updateDisplayFromPendingOp.

So here's the more idiomatic version using Option.map and a newStateWithPending helper function:

// Second version of addPendingMathOp 
// * Uses "map" and helper function
let addPendingMathOp services op state = 
    let newStateWithPending currentNumber =
        let pendingOp = Some (op,currentNumber)
        {state with pendingOp=pendingOp}

    state.display
    |> services.getDisplayNumber 
    |> Option.map newStateWithPending 
    |> ifNone state

And here's one using maybe:

// Third version of addPendingMathOp 
// * Uses "maybe"
let addPendingMathOp services op state = 
    maybe {            
        let! currentNumber = 
            state.display |> services.getDisplayNumber 
        let pendingOp = Some (op,currentNumber)
        return {state with pendingOp=pendingOp}
        }
    |> ifNone state // return original state if anything fails

As before, I'd probably go for the last implementation using maybe. But the Option.map one is fine too.

Implementation: review

Now we're done with the implementation part. Let's review the code:

let updateDisplayFromDigit services digit state =
    let newDisplay = services.updateDisplayFromDigit (digit,state.display)
    let newState = {state with display=newDisplay}
    newState //return

let updateDisplayFromPendingOp services state =

    // helper to do the math op
    let doMathOp (op,pendingNumber,currentNumber) = 
        let result = services.doMathOperation (op,pendingNumber,currentNumber)
        let newDisplay = 
            match result with
            | Success resultNumber ->
                services.setDisplayNumber resultNumber 
            | Failure error -> 
                services.setDisplayError error
        {display=newDisplay;pendingOp=None}

    // fetch the two options and combine them
    let newState = maybe {
        let! (op,pendingNumber) = state.pendingOp
        let! currentNumber = services.getDisplayNumber state.display
        return doMathOp (op,pendingNumber,currentNumber)
        }
    newState |> ifNone state

let addPendingMathOp services op state = 
    maybe {            
        let! currentNumber = 
            state.display |> services.getDisplayNumber 
        let pendingOp = Some (op,currentNumber)
        return {state with pendingOp=pendingOp}
        }
    |> ifNone state // return original state if anything fails

let createCalculate (services:CalculatorServices) :Calculate = 
    fun (input,state) -> 
        match input with
        | Digit d ->
            let newState = updateDisplayFromDigit services d state
            newState //return
        | Op op ->
            let newState1 = updateDisplayFromPendingOp services state
            let newState2 = addPendingMathOp services op newState1 
            newState2 //return
        | Action Clear ->
            let newState = services.initState()
            newState //return
        | Action Equals ->
            let newState = updateDisplayFromPendingOp services state
            newState //return

Not bad -- the whole implementation is less than 60 lines of code.

Summary

We have proved that our design is reasonable by making an implementation -- plus we found a missed requirement.

The trick is to recognize that the pattern "if something exists, then act on that value" is exactly the bind pattern discussed and .

But F# allows you to hide the complexity in a different way, by creating .

If there is a very strong "pipeline" feel, as in approach, then I prefer using an explicit bind.

In the , we'll implement the services and the user interface to create a complete application.

The code for this post is available in this on GitHub.

here
here
computation expressions
the ROP
next post
gist
previous post
Calculator image