After getting some help, understanding the problem I had trying to compile the code, in this question (Trouble understanding GHC complaint about ambiguity) Will Ness suggested I redesign my type classes to avoid a solution I was not completely happy with.
The type classes in question are these:
class (Eq a, Show a) => Genome a where
crossover :: (Fractional b) => b -> a -> a -> IO (a, a)
mutate :: (Fractional b) => b -> a -> IO a
develop :: (Phenotype b) => a -> b
class (Eq a, Show a) => Phenotype a where
--In case of Coevolution where each phenotype needs to be compared to every other in the population
fitness :: [a] -> a -> Int
genome :: (Genome b) => a -> b
I'm trying to create an extendible evolutionary algorithm in Haskell which should support different Genomes
and Phenotypes
. For instance one Genome
could be a bit array, another could be a list of ints, and the Phenotypes
will also be diverse from just a list of doubles representing troop movement in http://en.wikipedia.org/wiki/Colonel_Blotto, or it could represent an ANN.
Since a Phenotype
is developed from a Genome
the methods used must be quite interchangeable, and one Genome
class should be able to support multiple Phenotypes
by supplying a different develop method (this can be done statically in code, and does not have to be done dynamically at runtime).
The code using these type classes should, for the most part, be blissfully unaware of the specific types used, which is what lead me to ask the question mentioned above.
Some of the code that I want to adapt to these type classes are:
-- |Full generational replacement selection protocol
fullGenerational :: (Phenotype b) =>
(Int -> [b] -> IO [(b, b)]) -> --Selection mechanism
Int -> --Elitism
Int -> --The number of children to create
Double -> --Crossover rate
Double -> --Mutation rate
[b] -> --Population to select from
IO [b] --The new population created
fullGenerational selection e amount cross mute pop = do
parents <- selection (amount - e) pop
next <- breed parents cross mute
return $ next ++ take e reverseSorted
where reverseSorted = reverse $ sortBy (fit pop) pop
breed :: (Phenotype b, Genome a) => [(b, b)] -> Double -> Double -> IO [b]
breed parents cross mute = do
children <- mapM (\ (dad, mom) -> crossover cross (genome dad) (genome mom)) parents
let ch1 = map fst children ++ map snd children
mutated <- mapM (mutate mute) ch1
return $ map develop mutated
I understand that this code will have to be changed and new constraints will have to be added, but I wanted to show some of the code I have in mind using the type classes with. For instance, the full generational replacement above does not need to know anything about the underlying Genome
to function properly; it only needs to know that Phenotypes
can produce the Genome
that produced it so that it can breed them together and create new children. The code for fullGenerational
should be as general as possible so that once a new Phenotype
is designed or a better Genome
is created, it does not need to be changed.
How can the type classes above be changed to avoid the problems I was/am having with type class ambiguity while retaining the properties I want in the general EA code (which should be reusable)?