Calculator Walkthrough: Part 2
Testing the design with a trial implementation
Last updated
Was this helpful?
Testing the design with a trial implementation
Last updated
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.
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.
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:
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
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:
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:
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:
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.
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:
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:
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?
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
.
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
.
You can also tidy this up a bit as well by piping the result directly into defaultArg
, like this:
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:
Alternatively, we could create a local helper function instead, and connect it to the bind, like this:
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:
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
, 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.
We can rewrite getCurrentNumber
to be a bit more idiomatic by using pipes:
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.
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
.
and we'll need to add it to the CalculatorServices
structure too:
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.
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.
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!
With this computation expression available, we can use maybe
instead of bind, and our code would look something like this:
In our case, then we can write yet another version of updateDisplayFromPendingOp
-- our fourth!
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
.
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:
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:
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:
And here's one using maybe
:
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:
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.
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.