Search code examples
haskellcode-duplication

Similar functions operating on the same datastructure


I've got a data type of the following form:

type Orders                 = [Int]
data Score                  = Score Cost Penalty Penalty
type Trip                   = (Int, Cost, Orders)
type Trips                  = [Trip]
type DaySolution            = (Trips, Trips)
data Solution               = Solution { score         :: Score,
                                         skippedOrders :: Orders,
                                         monday        :: DaySolution,
                                         tuesday       :: DaySolution,
                                         wednesday     :: DaySolution,
                                         thursday      :: DaySolution,
                                         friday        :: DaySolution
                              } deriving(Show)

My 'main' datatype is solution. Now I want to implement some genetic/evolutionary algorithm.

This means I have to mutate Solution. Most of my mutations will need to operate on individual trip's.

For example, reverse the elements of a trip. Or Swap the trips of monday with tuesday. Or even swap the monday trips of solution 1 with the monday trips of solution 2.

For most mutations I need the following steps:

  1. I need randomness to determine what Trip(s) to mutate.
  2. Mutate the Trips
  3. Return the updated solution

Obviously (2.) would be unique for each mutation function. But (1.) and (3.) are quite similar.

What is a good way of implementing this without having to duplicate steps 1 (and 3) for each mutation function?


Solution

  • Concept: higher order functions

    A higher order function is a function that takes as input another function. Without realizing, you have probably used several higher order functions already (like map and foldl).

    Simply use a higher order function, something with signature:

    mutateGeneric :: (Trip -> IO Trip) -> Solution -> IO Solution
    

    so here the first argument is a function to mutate a trip, and your mutateGeneric function itself generates random numbers, performs a modification based on the function, and then returns the solution. If you do not perform IO (for instance because random numbers are generate otherwise), you can simply use:

    mutateGeneric :: (Trip -> Trip) -> Solution -> Solution
    

    Example

    Say you first generate an int between 0 and 4 (inclusive) to determine the day, next you mutate both trips of that day, and finally you return the mutated solution.

    First we are going to define some utility methods:

    getDay :: Int -> Solution -> DaySolution
    getDay 0 = monday
    getDay 1 = tuesday
    getDay 2 = wednesday
    getDay 3 = thursday
    getDay 4 = friday
    
    setDay :: Int -> Solution -> DaySolution -> Solution
    setDay 0 s d = s { monday = d }
    setDay 1 s d = s { tuesday = d }
    setDay 2 s d = s { wednesday = d }
    setDay 3 s d = s { thursday = d }
    setDay 4 s d = s { friday = d }
    

    Then mutation looks like:

    mutateGeneric modifier solution = do
        di <- randomIO :: IO Int
        tam <- modifier ta
        tbm <- modifier tb
        return $ setDay day solution (tam,tbm)
            where day = mod day 5
                  (ta,tb) = getDay day solution
    

    and now a modifier could for instance swap the first with the second Trips item:

    swapModifier :: Trips -> IO Trips
    swapModifier (a:b:s) = return (b:a:s)
    swapModifier x = return x
    

    Then finally you can say that your modification heurstic is:

    heuristic1 :: Solution -> IO Solution
    heuristic1 = mutateGeneric swapModifier
    

    The point is that you can simply construct another modifier like swapModifier and combine it with mutateGeneric. Of course the example above is rather simple. Furthermore perhaps you need to update scores and penalties. This you can do in the mutateGeneric function.