A program is a collection of decisions, loops , and declarations. Sometimes, though, you need to create a program dynamically. Functors are special constructors in a programming environment. A functor is a function with a state. With functors, you can dynamically build up an application and then have that application execute some type of logic.
In general, functors are not that useful. The scope of a functor is very specific to the domain of solving logic problems. For example, let's say that a word processor application is being coded. When the document is printed, the individual elements would need to be laid out on the virtual paper. Functors would be a good way of automating the layout calculation process.
Consider the following simple example of calculating the royalties on the book you are reading. Note that the values for the individual royalty figures are fictitious, but the formula is not. Typically, royalties are calculated using a formula that involves royalties according to specific countries and specific book sales. The formula is not complex, but it involves calculations and decisions. Such a formula cannot be represented using one single mathematical formula. In most cases, the formula would be written as shown in Listing 9.1.
public double calculateRoyalty( long countMarket1, long countMarket2) { double total = 0.0; if( countMarket1 < _pivot) { total += (_royalty1 * (double)countMarket1); } else { total += (_royalty1 * (double)_pivot); total += (_royalty2 * (double)(countMarket1 - _pivot)); } if( countMarket2 < _pivot) { total += (_royalty1 * (double)countMarket2); } else { total += (_royalty1 * (double)_pivot); total += (_royalty2 * (double)(countMarket2 - _pivot)); } return total; }
Listing 9.1 illustrates the classic approach to writing business logic that involves mathematical calculations. The problem with this approach is that it's not easy to maintain the application. If a formula requires any change, you must then meticulously inspect the code and make the necessary modifications. Using functors, we can rewrite Listing 9.1 into something that is maintainable and extendable.
Tables 9.1 and 9.2 contain the abbreviated details necessary to use the functor package.
Item | Details |
---|---|
CVS repository | jakarta-commons |
Directory within repository | functor |
Main packages used | org.apache.commons.lang.functor (contains the main interfaces) |
All of the subpackages reference some implementation of a specific type of functor. For example, the subpackage composite contains composite-type functors. |
Interface | Details |
---|---|
[functor].BinaryFunction | A function-based functor that accepts two object parameters and returns an object. |
[functor].BinaryPredicate | A validator-based functor that accepts two object parameters and returns a Boolean value. |
[functor].BinaryProcedure | A function-based functor that accepts two object parameters but returns no values. |
[functor].Function | A function-based functor that has no parameters but returns an object. |
[functor].Predicate | A validator-based functor that has no parameters but returns a Boolean value. |
[functor].Procedure | A function-based functor that has no parameters and returns no values. |
[functor].UnaryFunction | A function-based functor that accepts an object parameter and returns an object. |
[functor].UnaryPredicate | A validator-based functor that accepts an object parameter and returns a Boolean value. |
[functor].UnaryProcedure | A function-based functor that accepts an object parameter but returns no values. |
The functor package has many interfaces that are identical to those in the Collections package. For example, the interface Predicate , which exists in the Collections package, also exists in the functor package. In either package, they serve the same purpose. The Collections package contained decorators, which alter the characteristics of the individual collection. This is very similar to functors, except that the scope of using the concept functor was restricted.
Rewriting a mathematical formula using functors is not a simple task. It is simpler to write a mathematical formula with a few decisions than to use functors. To write maintainable and extendable functors, you need to decompose the core formula into its core parts .
Let's start with the simplest part of the royalty formula from Listing 9.1, which is the calculation of royalties based on the simple formula defined by Listing 9.2.
Total Income = Royalty * Sales * Price per book
To convert the formula in Listing 9.2 into functors, the first step is to convert the formula into reusable calculation blocks. In the case of Listing 9.2, the formula is a series of multiplications. Therefore, the simplest reusable calculation block would be to multiply the individual blocks together. Listing 9.3 is an example of how this could be written.
double result = royalty( priceperbook( sales()))
The type of formula used in Listing 9.3 is called an unary function . An unary function is a function that has one parameter and returns a value. In the functor package, the unary function is represented by the interface UnaryFunction .
The next step would be to create an implementation that derives from the UnaryFunction and makes the logic of Listing 9.3 possible. Listing 9.4 shows how to implement the logic of Listing 9.3.
abstract class Formula implements UnaryFunction { protected abstract double calculate( double input); public Object evaluate(Object obj) { if( obj == null) { return new Double( calculate( Double.NaN)); } else { Double input = (Double)obj; return new Double( calculate( input.doubleValue())); } } }
In Listing 9.4, the abstract class Formula implements the UnaryFunction interface. The interface UnaryFunction exposes one method, evaluate . The method evaluate has both an input parameter of type Object , and a return value of type Object . The object can be anything, but in the context of the royalty formula, the object represents a Double class instance.
Converting a Double class instance to a double value can be tedious . To simplify the individual calculations, the abstract method calculate is defined. The abstract method calculate has the input and output parameter a double value.
Looking closer at the evaluate method implementation in Listing 9.4, notice how if the parameter obj is null , the value Double.NaN is set as a parameter. The reason for this has to do with the problem of starting the decorator calculation. Go back to Listing 9.3; the method sales requires no input parameter. Therefore, to ensure that the chain of calculations starts that way, the method evaluate is called with a null object, which then generates a Double.NaN value. If the value Double.NaN is used by accident in the calculation, then the entire calculation will be incorrect.
Going back to Listing 9.2, three classes need to be implemented, which represent the individual parts of the calculation. Listing 9.5 is the class implementation for the number of books sold.
abstract class Market extends Formula { private long _totalSold; public Market() { } public Market( long totalSold) { _totalSold = totalSold; } protected abstract double getFactor(); public double calculate( double input) { return _totalSold * getFactor(); } }
The class Market in Listing 9.5 represents the number of books sold in a particular market. In the book industry, certain markets have certain factorizations. For example, selling an English-language book in a foreign market may involve extra costs, thus reducing the overall royalty. Therefore, the implementation of the method calculate requires that the total number of books sold be multiplied by a factor. The total number of books sold is the variable _totalSold . And the factor is represented by the abstract method getFactor . Also notice how the parameter input is ignored in the calculation. This is per the requirements defined earlier by the formula in Listing 9.2.
Listing 9.6 represents the remaining implementations of the price and royalty parts of the formula.
class Royalty extends Formula { private double _amount; public Royalty( double amount) { _amount = amount; } public double calculate( double input) { return _amount * input; } } class Price extends Formula { private double _amount; public Price( double amount) { _amount = amount; } protected double calculate(double input) { return input * _amount; } }
In Listing 9.6, the classes Price and Royalty have a calculate method implementation that calculates the input with the amount of each class.
The individual calculations are chained together using the CompositeUnaryFunction , as is shown in Listing 9.7.
public static UnaryFunction getSimpleContract( Market market, Price price, Royalty royalty) { CompositeUnaryFunction calculation = new CompositeUnaryFunction(); calculation.of( price).of( royalty).of( market); return calculation; }
In Listing 9.7, the method getSimpleContract has three parameters. Each of the parameters represents one part of the formula. The thought behind the method getSimpleContract is to create a contract by stringing together a number of classes that calculate the royalty. You can use the class CompositeUnaryFunction to string the individual calculations together using the method of .
To calculate the royalty of a single contract, Listing 9.8 is used.
Double value = (Double)contract.evaluate( null);
However, overall contracts are not single contracts. For example, the European market has a different contract than the North American market. To finish the entire formula, we combine everything and manage it using the class defined in Listing 9.9.
class Author { Vector _contracts = new Vector(); private double _total; public void addContract( Object market) { _contracts.add( market); } class Sum implements UnaryProcedure { public void run(Object obj) { UnaryFunction contract = (UnaryFunction)obj; Double value = (Double)contract.evaluate( null); _total += value.doubleValue(); } } public double calculateRoyalty() { _total = 0.0; Sum sum = new Sum(); CollectionAlgorithms.foreach( _contracts.iterator(), sum); return _total; } }
In Listing 9.9, the class Author represents an individual author. The author has associated contracts and can have his royalty calculated. The contracts are stored in a Vector class instance. A contract can be added using the method addContract . The input parameter market is the class instance of the interface UnaryFunction .
In the method calculateRoyalty , the list is iterated using the class method CollectionAlgorithms.foreach . The first parameter is an iterator that will be iterated. The second parameter is an instance of the interface UnaryProcedure , which is the class Sum . The iterator is necessary because the individual totals from each contract are added together to create an overall total. The class Sum keeps a running total and is responsible for calling the chained contract method.
This is where the magic of functors comes into full view. The class method Sum.run typecasts the parameter obj to the interface UnaryFunction . This is legal because, going back to Listing 9.7, the contract that is generated by the method getSimpleContract is an UnaryFunction interface instance. Calling the UnaryInterface interface instance calls an implementation that iterates the chained formula and generates a result. In the sample contract, the result is generated using a multiplication. However, that result could have been generated using a database lookup. In either case, the caller of the functor does not care and expects only a Double class instance result. Listing 9.10 shows how to create the individual contracts.
Author author = new Author(); author.addContract( Contract.getSimpleContract( new BasicMarket( 10), new Price( 10.00), new Royalty( 0.35)));double value = author.calculateRoyalty();
In Listing 9.10, the individual contracts are created by calling the class method Contract.getSimpleContract . Notice how the individual parts of the formula are based on the instantiation of the correct class, with the correct numeric value.
Cynics might say that such a lengthy explanation to carry out a formula as simple as that represented by Listing 9.2 is an example of complicating a simple solution. This is partially correct, but an equation often masks the complexity or incompleteness of the entire solution. Remember that when you're calculating the royalty of a book, there are different levels of royalties depending on the region and price levels sold. The same equation is used, but different numbers are inserted into the equation. Those different numbers are what makes it difficult to represent a universal equation to solve all problems.
Going back to Listing 9.1, the code took into account a high level of royalties and a low level of royalties that depend on the total sales count. This means that we have to decide which formula to use to do the calculation, and then alter the calculations. The first part of the formula dictates if a sales level is higher or lower than a certain level, the class ConditionalUnaryFunctor can make a decision and then can call the correct functor. Listing 9.11 is an example of using the class ConditionalUnaryFunction to illustrate a decision.
ConditionalUnaryFunction conditional = new ConditionalUnaryFunction( new MyTestUnaryPredicate(), new TrueValueUnaryFunction(), new FalseValueUnaryFunction());
In Listing 9.11, the decision class is the predicate class MyTestUnaryPredicate . If the predicate class returns a true value, then the unary function of the class TrueValueUnaryFunction is called. Otherwise, a false value will call the unary function in class FalseValueUnaryFunction . If we go back to the royalty formula, that means a level predicate class has to be created. This is shown in Listing 9.12.
class SalesBreakpoint implements UnaryPredicate { private Market _market; private long _level; public SalesBreakpoint( Market market, long level) { _market = market; _level = level; } public boolean test(Object object) { if( _market.getTotalSold() < _level) { return false; } else { return true; } } }
In Listing 9.12, the class SalesBreakpoint implements the interface UnaryPredicate . The interface UnaryPredicate has one method, test . The method test is used to compare the value and then return either a value of true or false . In the implementation of the method test in Listing 9.12, the parameter object is not used because the value of object is a Double class instance. What needs to be compared is the total sales count in a particular market. Hence, the constructor of the class SalesBreakpoint requires a Market class instance and the level used to distinguish between the higher and lower royalty rates.
Depending on the output of the class SalesBreakpoint , a specific calculation is performed. This specific calculation is nothing more than a specific version of a contract. For example, if the sales count is below the required level, then a contract of a lower royalty is applied. This means the method getSimpleContract can be reused, except that different initial values are defined. If a total sales count is higher than a requested level, the royalty rate is calculated based on two royalty rates: higher level and lower level. This in effect means that there are two contracts. Yet again, the method getSimpleContract can be reused. The key is in being able to string the contracts together properly so that the correct royalty is generated.
We also have to change the calculation of the total sales count for a particular contract. The way to handle this is to create a decorator that adjusts the sales figure, as shown in Listing 9.13.
private static Market getDecoratorMarketLow( final Market market, final long level) { return new Market() { private Market _market = market; private long _level = level; public double getFactor() { return 0.0; } public Object evaluate(Object obj) { if( _market.getTotalSold() < _level) { return new Double( _market.getTotalSold() * _market.getFactor()); } else { return new Double( _level * _market.getFactor()); } } }; } private static Market getDecoratorMarketHigh( final Market market, final long level) { return new Market() { private Market _market = market; private long _level = level; public double getFactor() { return 0.0; } public Object evaluate(Object obj) { if( _market.getTotalSold() < _level) { return new Double( 0.0); } else { long actualCount = _market.getTotalSold() - _level; return new Double( actualCount * _market.getFactor()); } } }; }
In Listing 9.13, two anonymous classes that act as decorators and adjust the total sales count are created. The adjustment depends on whether the calculation is based on the high royalty figure or a low royalty figure. Listing 9.14 strings everything together to create a contract that generates the correct royalty figure.
public static UnaryFunction getMinimumValueContract(Market market, Price price, Royalty royaltyHigh, Royalty royaltyLow, long level) { UnaryFunction calcLow = Contract.getSimpleContract( Contract.getDecoratorMarketLow( market, level), price, royaltyLow); UnaryFunction calcHigh = Contract.getSimpleContract( Contract.getDecoratorMarketHigh( market, level), price, royaltyHigh); UnaryFunction highLow = Contract.getDecoratorHighLow( calcHigh, calcLow); ConditionalUnaryFunction condition = new ConditionalUnaryFunction( new SalesBreakpoint(market, level), highLow, calcLow); return condition; }
In Listing 9.14, the method getMinimumValueContract has five parameters that make up the royalty calculation. In the implementation of the method getMinimumValueContract , two contracts are generated because the low sales count contract used for the combination royalty calculation can also be used as a low sales contract. The variable calcLow represents the low royalty contract. The variable calcHigh represents the high royalty contract. The variable highLow represents the combination royalty contract. Lastly, the variable condition combines the low royalty contract with the combination royalty contract to produce an overall contract. The individual contracts were created using the method getSimpleContract .
Next, Listing 9.15 shows how to create the overall generic contract that represents the author.
author.addContract( Contract.getMinimumValueContract( new NorthAmericanMarket( 10), new Price( 10.00), new Royalty( 0.35), new Royalty( 0.25), 5)); author.addContract( Contract.getMinimumValueContract( new EuropeanMarket( 10), new Price( 10.00), new Royalty( 0.35), new Royalty( 0.25), 5)); double value = author.calculateRoyalty();
In Listing 9.15, the method addContract is called twice because a royalty total needs to be generated for both the European and North American markets. We do not need to adapt the method calculateRoyalty for this new contract because both contracts generated a UnaryFunction interface instance.
The functor package has many different classes that help a developer create a complicated functor chain that performs some complex operation dynamically. However, it's beyond the scope of this book to discuss these classes. In addition, the classes are well documented by the generated Java Docs. What is not even close to apparent, though, is how to make use of functors. Functors are not your everyday programming constructs. Functors can make a calculation simpler or devilishly complex and unusable. Therefore, it's important that we define a strategy to design functors:
Decompose a formula into its core parts that make up all variations of the calculation known at the time. The core parts of our royalty example are royalty rate, price of book, and sales count. All of the core parts are required in all calculations, but you may tweak and adjust them to make the overall formula work. For example, we can adjust the sales count to account for the market conditions.
Create a simple version of the formula. Do not attempt to solve the entire formula in one step. Create a simple version of the formula that uses all the core parts. The objective is to hopefully find a version of the formula that can be reused in some fashion. In our royalty example, that meant generating a simple royalty calculation regardless of market or royalty adjustments.
Decompose the formula in multiple smaller formulas. In this step, the idea is to decompose a complex formula into smaller formulas that are combined. Ideally, the smaller formulas should bear some resemblance to the simple version of the formula. In our royalty rate, the individual parts were decomposed into a simple formula, which was combined into formula blocks by decorating parts of the simple formula.
Combine the smaller formulas into a big formula and see if everything works. Once the individual formula blocks have been defined, they are combined into an overall calculation that is used to calculate a result given a set of conditions.
In addition, remember these notes when designing your own functors:
Do not be afraid to decorate components . Decorating a component of the formula is a very effective way of altering the characteristics without actually cloning or modifying the original component.
Do not modify the individual components of the formula. The components are considered immutable. Modifying them may result in a very unstable and unpredictable result because, essentially , functors are state driven; if the original state is modified during the execution of the formula, other parts of the formula might change in unforeseeable ways. These sorts of bugs are extremely difficult to locate and fix. You can add other items to the state, so long as the original is kept intact.
Use anonymous classes and inner classes to create your own functors. Anonymous classes are state driven and not subject to extending or modification. In addition, anonymous classes make it very easy to implement immutable objects. They are very useful when you need specific functors that are not provided by the functor package.
On the CD It is recommended that you look at the sources provided in this book's CD-ROM because they show a working copy of the royalty functor described in this chapter. The function example is found in the file at the location [CDROM]/jseng/src/com.devspace. jseng .algorithms/FunctorText.java.