layout | title | description | categories | seriesId | seriesOrder | |
---|---|---|---|---|---|---|
post |
Calculator Walkthrough: Part 2 |
Testing the design with a trial implementation |
|
Annotated walkthroughs |
2 |
In this post, I'll continue developing a simple pocket calculator app, like this:
In the previous post, 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.
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.
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()
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
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?
The trick is to recognize that the pattern "if something exists, then act on that value" is exactly the bind
pattern discussed here
and here.
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.
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
, otherwiseNone
.
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.
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
So far, we've being using "bind" directly. That has helped by removing the cascading if/else
.
But F# allows you to hide the complexity in a different way, by creating computation expressions.
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.
- If there is a very strong "pipeline" feel, as in the ROP approach, then I prefer using an explicit
bind
. - 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
.
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.
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.
We have proved that our design is reasonable by making an implementation -- plus we found a missed requirement.
In the next post, we'll implement the services and the user interface to create a complete application.
The code for this post is available in this gist on GitHub.