Introduction
My previous article was Implementing Continuations in a Generic Way - The Usage of the Option Type in C# - Make Code More Clear. The explanation was a bit concise, because people should concentrate on the code. I think it was too concise, so in this article I will explain a little bit more and also add some additional features in which the concept is brought more to the front.
An essential problem in software development is that a functional design cannot be directly translated into code. In this article, I will try to bridge this gap by concentrating on the point of Business Flow.
For example: You have a firm that deals with Hardware repairs. On certain conditions (Warranty), you get a new one, in others you get a quotation. In a functional design, this can be written down in a use case scenario, or in some diagram. The functional outline is translated by a software developer in all kinds of technical database calls, error handling, Events, Messaging, If-Then logic, etc... If a user asks the functional analyst what happens if a certain condition is met, a look at the diagram will do. If you ask the developer, he has to go through all these calls, from top to bottom, from assembly to assembly to find out. Perhaps he has put all business logic in a Controller (MVC-pattern), but even then the code is full of technical stuff (error handling) and in most cases you still need jumping through multiple methods.
So, I wondered, can you separate the flow of a program from its implementation. In other words, can you make a skeleton of linked subsequent calls that reads from left to right and that tells exactly how the logic is outlined, not how exactly it is technically implemented.
What do you need for this? First, linking calls can only be done if the types are the same. Secondly, the definition must be separate from the execution. The Func<T, T>
is the first part of the answer. You have a Function
that accepts a (generic) type T
and returns the same type T
, that can be the input of the next function with the same signature. In the end, you get something like:
Func<Func<T , T>, T>
You can write the same using lambdas and you get:
((x=> x) => X)
Looking at this, it is not difficult to see the problem. It is not an easy read if you got a lot of functions to link. And it become worse if your logic ‘branches’. With this, I mean that you first want to check if a Hardware is Valid, and based on the result (True
or False
), the logic is branched to either a set of Warranty
functions or a set of ResendBackToCustomer
-functions. On their turn, both branches can be branched also, etc... The above lambda will become very difficult to read.
To solve this, I wanted to able to define a group of functions, store this in a variable and use this in the end flow. This makes the code readable.
The second part of the solution (splitting definition and execution) can be done because a Func<T, T>
can be stored in a collection as a MultiCastDelegate
. The collection holds these delegates as definitions but doesn’t execute them immediately. I had to wrap the Funcn<T,T>
in something that supports this mechanism.
The final solution has also to take care of exception handling and ‘none’ situations. The latter must happen if your Function<T,T>
cannot return a decent T
. Compare this with a null
value in case of a database. To accomplish this, you can wrap the T
into an Option
. Option
is an abstract
class with two concrete implementations Some<T>
and None<T>
. By returning an Option
, you can either return a None
or a Some
, which speaks for themselves.
To accomplish my goals, I started with the Option
and added to it branch functionality, delayed execution, and flow types like IFTHENELSE
.
The Option Class. How It Works
First a warning. In traditional OO, you define in one class the data structure and the methods that work on them. In this – more functional inspired approach – you define your data structure in a data object and pipeline this data object through a series of linked functions. As an OO-guy, you have to think differently!
The option supports the following flow types: AndThen, AndThenCaseCheck, AndThenCheck, AndThenIfElse
the meaning of which is explained by example. In the accompanying code example, I have a Hardware
data object. You first wrap this into an Option
:
Option<Hardware> somehw = Option.Some(hw);
And then can do:
Somehw.AndThen(HardwareBusiness.PurchaseWarranty)
......AndThen(HardwareBusiness.ConditionWarranty);
It is not hard to see what this means: First, call the function PurchaseWarranty
and then call ConditionWarranty
. If you look in the Option
class, you see that AndThen
only adds the function to an instance of the class Funcbranch
(a branch that holds a collection of functions). An option can have one or many Funcbranches
. In this case, the above functions are added to the root funcBranch
.
You can also do:
FuncBranch<Hardware> fncWarranties = new FuncBranch<Hardware>();
fncWarranties.AndThen(HardwareBusiness.PurchaseWarranty)
.AndThen(HardwareBusiness.ConditionWarranty);
Somehw.AndThen(determineWarrantyFunc)
In this case, you define a complete independent FunchBranch
and add this complete branch to the AndThen
flow type. Studying the code in the option
class, you see that the passed FuncBranch
is added to the collection of FuncBranches
. To the root funcBranch
we add a dummy function, that’s doing nothing (compare the lambda (x=<x))
, but bookmarked with the number of the added funcbranch
. If this dummy function is executed, the code hits the bookmark and branches execution to the functions of the added funcBranch
. That’s how branching is implemented.
If you call AndThenCaseCheck
, you have to apply a list that per list item contains two elements, a function that takes an Option
and returns a boolean and a function to apply when the returned boolean is true
. The andthenCheck
expects the same option
/Boolean
function and the accompanying apply function in case of a true
and of a false
. The AndThenIfElse
is a bit different. When the First
function results in None
, the second function is executed. In all these cases, you either provide a single function or a FunchBranch
(collection of function). In the following examples, I give you a few compositions you can make.
Somehw.AndThenCheck(HardwareBusiness.isValid
, (x => x)
, HardwareBusiness.PurchaseWarranty).Exec();
Means If HardwareBusiness.isValid
returns true
, do nothing (that’s the meaning of (x=> x))
, otherwise execute the function HardwareBusiness.PurchaseWarranty
.
somehw.AndThenCheck(x => {
Hardware hw3 = HardwareBusiness.GetHardWare(x);
return (hw3.Brand== "HP");
},
HardwareBusiness.PurchaseWarranty,
HardwareBusiness.getQuotationFixedPrice
).Exec();
Means, you perform an inline check and then proceed with the ontrue (PurchaseWarranty)
and onfalse (getQuotationFixedPrice)
delegates.
FuncBranch<Hardware> fncWarranties = new FuncBranch<Hardware>();
fncWarranties.AndThen(HardwareBusiness.PurchaseWarranty)
.AndThen(HardwareBusiness.ConditionWarranty);
somehw.AndThenCheck(HardwareBusiness.isValid,
fncWarranties,
x => {return Option.None<Hardware>();}
).Exec();
Means, if the isValid
check succeeds, execute the functions of the fncWarranties
branch, otherwise return None
(meaning stop any further processing). These are just examples that show how you to make your compositions.
Code Example
The accompanying fictitious example - it doesn’t resemble real cases - is about hardware. On the basis of checks, it is determined whether there is a warranty, which pricing model has to be applied and how to swap. Sometimes the checks apply to all, in other cases to some. Sometimes it is determined automatically, sometime the user has to decide. You swap in case of an in-warranty and you make a quotation in case of an out-warranty.
If you run the code, you select a hardware from the list. In the comment, you see what the selected example is about. In the textbox
below, you will see a trace of what’s happening. The core of the code is in the class HardwareController
, method processHardware
. In this method, all the outlining is defined. Compare this method with a traditional approach, where a programmer will code it everywhere.
processHardware
is only about outlining the logic first before executing it. For instance, the method caseWarrantyChecksPerMark
determines that Sony’s have to be pipelined through the fncWarrantiesExt
and the other marks through the fncWarranties
. The warranty checks themselves (PurchaseWarranty
, etc.) are not executed.
If you look at the individual methods, you see very simple checks that often only set properties. The branching logic is namely already taken care of.
In addition, I implemented a mechanism where all ‘transactional methods’ are not called directly. I grouped these transactions methods in the region Exe
cutes
of the HardwareBusiness
class. The method updateisInvalid
should update a database. The class Hardware
has a property Executes
of type FuncBranch
, taking one step further the principle of outlining first before executing. In some cases, this is not a bad thing to do.
Take the following implementation that’s used in the example:
public static Boolean isValid(Option<Hardware> input)
{
Hardware hw = GetHardWare(input);
if (string.IsNullOrEmpty(hw.TypeName))
{
Report(hw, "No type -> End flow");
hw.Executes.AndThen(updateisInvalid);
hw.Executes.AndThen(sendBack);
return false;
}
hw.Executes.AndThen(updateValidStatus);
return true;
}
Instead of calling updateValidStatus,updateisInvalid,sendBack
directly, these methods are added to hw.Executes
, which is of type FuncBranch<Hardware>
. The last line in the processHardware
method is:
somehw.AndThen(Option.ReadValue<Hardware>(somehw).Executes).Exec();
By now, you probably see what’s happening. hw.Executes
returns a FuncBranch
, that can be input for the AndThen
method of the Hardware
Option. The Exec()
will execute all this.
In this case, this implementation pays off in case of an error. If you hit the free price scenario, you must enter a price. If you input a non-numerical character, the price will error. Because of this error, the flow that follows will not be executed. In this case, the transactional methods are not called. You don’t need to code for this explicitly, because this logic is part of the Option
class.
To the option
class, I added a TraceEventHandler
. If somebody adds itself as a listener, it will get all the methods that are called. In the example, this is done with these calls.
Option<Hardware> somehw = Option.Some(hw);
somehw.TraceInfo += new Option<Hardware>.TraceEventHandler(somehw_TraceInfo);
Making it extremely simple to trace the calls that make up the flow of the application.
- Bart