4.4 Dataflow & Stream Programming
4.4.7 Managing stochastic components & unsafePerformIO
Many metaheuristics make use of stochastic components, either for perturbation, as seen in Simulated Annealing, or to introduce variation in decision making pro-cesses, such as is seen in Genetic Algorithms. However the use of random num-bers, functions that give different outputs each time they are called on the same parameters, conflicts with the concept of pure functions. Streams present one solution to this issue, by creating stream transformations, pure functions which transform one stream into another. For example, the following function f incre-ments every input value by a random value between 0 and 1;
f:: System.Random.RandomGen g ⇒ g → [Double] → [Double]
f g= zipWith (+) (randoms g)
The use of stream transformations can also be seen in Figure4.4. In this example r is a stream of random values, and is used to define a stochastic perturbation transformation p, which will perturb each solution on its input stream, giving a stream of perturbed solutions. The stream p is created through a pure computation (subject to the source of the random values), which provides stochastic effects on the values in the input stream, but does not require the user to explicitly manage the threading of the RNG itself.
Pseudo RNGs in computer programs require IO to be created, using properties of the system such as the clock for an initial seed. In Haskell this can be done in
66
the following manner, where the function f is applied to two different RNGs, creating two different operations, and then composed together.
main= do g1 ← newStdGen g2← newStdGen
print$ f g1◦ f g2 $ repeat 0
The general pattern for the construction of a metaheuristic in this manner is ex-pected to follow the following form;
main= do g ← newStdGen
let vs= loopS (... ◦ stochasticComponent g ◦ ...
) seedSolution print vs
Typically each metaheuristic strategy will only contain a few stochastic com-ponents, so this approach is acceptable and does not incur a significant over-head. However ideally the library should be as simple as possible to use, and this approach introduces a small degree of book keeping. The overhead can be reduced through the introduction of impure computations through the function unsafePerformIO. There are two key issues that must be considered in using unsafePerformIO, the order of the evaluations and the number of evaluations of each expression.
Haskell does not give any guarantees of the order in which functions are eval-uated, so care must be taken that the logic of the program is not undone by compu-tations occurring in different orders, such as could happen to an algorithm which depends on the particular order of memory access and update. However in this case unsafePerformIO is being used for the construction of RNGs only, with the all other operations being pure functions. So there is generally no concern for the order in which RNGs are created, as long as they are suitably unpredictable.
The number of evaluations of each expression is more of a problem, for exam-ple;
g= zipWith (+) (unsafePerformIO $ newStdGen >>= return ◦ randoms) k= g ◦ g
Should each usage of g be the same transformation, with the same threaded RNG, or two different transformations? In this Thesis we typically want them to be different. This can be achieved by forcing inlining using the following Pragma.
{-# INLINE g #-}
This instructs Haskell to replace every instance of g with the body of g, causing k to be rewritten as;
k= zipWith (+) (unsafePerformIO $ newStdGen >>= return ◦ randoms)
◦ zipWith (+) (unsafePerformIO $ newStdGen >>= return ◦ randoms) There is still a danger that the Haskell compiler could search for and share common sub expressions, such as the repeated expression in k above. This issue should continue to be a consideration in future uses of this technique, however at the time of writing this approach has been tested on the current version of the Haskell compiler and it has been seen to work correctly.
4.5 Summary
This chapter has looked at several methods for implementing metaheuristics in Haskell, moving from a direct implementation of imperative concepts, towards the construction of the search strategies in terms of mutually recursive streams.
The following chapters approach metaheuristics, not explicitly defining mutually recursive streams, but using the composition of stream transformations, and will be broken down as follows:
Chapter5 details the basic library of combinators and how the combinators may be used to implement each of the major metaheuristic algorithms.
Chapter6 extends the use of the stream combinators into the expression of low level operators commonly used to manipulate combinatorial problems.
Chapter7 discusses some perspectives on hybridisation of metaheuristics and shows how the stream combinators can be used to enable implementation of hybrid algorithms.
Chapter 5
Metaheuristic Combinators
Chapter4proposed streams and data flow programming as a suitable approach for implementing metaheuristics, which provided flexibility to the programmer while fitting well with the functional foundations of Haskell. This chapter elaborates upon the stream based approach providing a library of combinators for manipu-lating the structure and imposing computations upon streams. It then shows how these combinators may be used to implement the five metaheuristic families that were named in the introduction and how the low level operators such as perturba-tion and recombinaperturba-tion also include funcperturba-tionality provided by the library.
5.1 Types for stream transformations
Two type synonyms for the standard Haskell list are introduced to improve read-ability, while enabling code reuse of functions over streams from the standard libraries.
• type Stream a = [a], where there is an unenforced promise for the list to not end; and
• type List a = [a], where there is an unenforced promise for the list to be finite.
The List type will be used to capture components of metaheuristics such as pop-ulation and neighbourhood and in this way it abstracts a commonality between
68
these two, which is not generally shown in metaheuristics. A more generic ap-proach, such as an abstracted group type could also be used, but to enable reuse of existing Haskell functions it is easiest to use the standard list.
While implementing search strategies the composite types Stream(List a) and List(Stream a) are frequently encountered. It is much rarer to find operations resulting in the type Stream(Stream a). Transformations between between basic streams and streams of lists are particularly common, corresponding to the expan-sion and contraction of choices suggested in Chapter 2. These common patterns are captured as the following types to simplify later function definitions.
• type ExpandT a b = Stream a → Stream (List b)
• type ContraT a b = Stream (List a) → Stream b