Haskell: Abstracting a Genetic Algorithm
Asked Answered
D

2

17

I'm new to the world of Haskell programming and I'm cutting my teeth on a simple genetic algorithm for finding good solutions to the Travelling Salesman problem. I am representing the solutions as permutations on Integers and so I have this type synonym

type Genome = [Int]

The algorithm itself is a set of functions which operate on the solutions:

mutation :: Genome -> Genome
selectParents :: [Genome] -> [Genome] -> [Genome]
crossover :: Genome -> Genome -> (Genome, Genome)
selectSurvivors :: [Genome] -> [Genome] -> [Genome]

I'm not sure how much of my code is relevant to my question so please ask if more details are needed. One thing that might be worth mentioning is that the type signatures above are actually simplified, I am in fact using the State monad to carry around an StdGen so all of these functions actually return stateful computations.

There are several things which I would like to do with this but can't quite get my head around. I want to make it possible to choose different representations for the solutions, it seems to me that this would be a natural place to use a type class, so that Genome would be the type class and [Int] a specific instance of this Genome.

Now, I want to be able to experiment with the implementations, and I want to be able to use the code in other projects. Using a type class like this would require that every new algorithm I create would require me to create another instance of Genome, is this a good way to go about creating a library?

One bonus question, just a thing that's been bothering me, is there any way to create something like a type synonym for a function so that if I'm writing a function which takes functions as arguments I can write the synonym rather than the whole type signature of the function i.e so that something like the following would work.

type someFunc = [Int] -> [Int] -> Int
someOtherFunc :: someFunc -> [Int] -> Int

Right, hopefully that's a lucid enough explanation of the problem, feel like I've missed the really obvious answer but it hasn't jumped out at me. Cheers

Demean answered 15/2, 2011 at 22:28 Comment(9)
what functions would the instances of Genome need to define? mutation, selectParents, etc? Or is there a smaller (and simpler) set of functions that you can define those functions in terms of?Kleinstein
I could be wrong but I think those functions about as simple as they can be. Maybe I'm just trying to make it more general than is possible.Demean
Sounds like this would spark good discussion on a mailing list. See Haskell mailing listsMehalek
On the type alias issue, exactly the code you provided should work. Does it not?Zosi
@sclv, type aliases must be capitalized. Other than that, it ought to work.Freedman
You should replace your selectParents and selectSurvivors functions with a fitness function to determine the fitness of a genome in your typeclass. This should prevent repeating some boilerplate for each implementation.Mccarter
The capitalisation thing was the problem. I posted this question on the mailing list and I have had some more ideas of my own, I'll post my solution when I have it.Demean
On the type alias, the someOtherFunc won't work even after the capitalization is fixed because types aren't first class in Haskell.Caducity
@ja -- No. I just double checked it to make sure I wasn't missing somedthing obvious and it worked.Zosi
M
7

Unfortunately, the ideal solution usually depends on your problem domain. This blog post talks about the typeclass approach and the bitwise approach. I personally think a hybrid approach is best if you want flexibility. If there is a good bitwise mapping, you can define it, and the implementation is derived from that, otherwise you can implement the crossover and mutate manually.

ja's method is actually not going to work. Some of your genome functions are going to need random input, which you can get by running in the state monad with a random number generator like this thread

class Genome a where
    fitness :: a -> Int
    breed :: (RandomGen g, MonadState g m) => a -> a -> m a
    mutate :: (RandomGen g, MonadState g m) => a -> m a

Then you have common functions that operate on sets of genomes, regardless of implementation.

selectParents :: (Genome a, RandomGen g, MonadState g m) => [a] -> m [a]
selectSurvivors :: (Genome a, RandomGen g, MonadState g m) => [a] -> m [a]

If you do have a good bit mapping, you can just define fixed functions on BitArrays (notice that each will have to take the fitness function as a parameter)

breed :: (RandomGen g, MonadState g m) => BitArray -> BitArray -> m BitArray
mutate :: (RandomGen g, MonadState g m) => BitArray -> m BitArray
selectParents :: (RandomGen g, MonadState g m) => (BitArray -> Int) -> [BitArray] -> m [BitArray]
selectSurvivors :: (RandomGen g, MonadState g m) => (BitArray -> Int) -> [BitArray] -> m [BitArray]
Mccarter answered 19/2, 2011 at 9:12 Comment(1)
Use the MonadRandom class from the package of the same name.Freedman
C
2

Yes, using a type class to represent a genome is a good way to go. Something like this:

class Genome a where
   mutation :: a -> a
   selectParents :: [a] -> [a] -> [a]
   crossover :: a -> a -> (a, a)
   selectSurvivors :: [a] -> [a] -> [a]
instance Genome [a] where
   mutation l = l
   selectParents l1 l2 = l1
   crossover g1 g2 = (g1,g2)
   selectSurvivors l1 l2 = l1
data Tree a = Leaf a | Branch [Tree a]   
instance Genome (Tree a) where
   mutation t = t
   selectParents l1 l2 = l1
   crossover g1 g2 = (g1,g2)
   selectSurvivors l1 l2 = l1

As for instancing a new datatype for each algorithm, you can include a few instances in your library, but there's no problem instancing new ones - that's the point !

Caducity answered 16/2, 2011 at 17:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.