4

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)?

Community
  • 1
  • 1
Nordmoen
  • 215
  • 1
  • 7
  • Translate the type class to the equivalent dictionary: see [this](http://www.haskellforall.com/2012/05/scrap-your-type-classes.html) and [this](http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/). – Gabriella Gonzalez Apr 02 '13 at 14:49
  • @GabrielGonzalez Hm, I might misunderstand, but how will that help solving the problems I was having with ambiguity? I understand that the style might be cleaner, but I don't see how translating what I already have into data declarations will solve the problem. – Nordmoen Apr 02 '13 at 15:35
  • Sorry, I misunderstood what you meant by ambiguity. Ignore me. – Gabriella Gonzalez Apr 02 '13 at 20:24
  • 2
    great job btw describing your problem in such a detail, really made it that much easier to deal with it. I wish every question on SO was like that! :) – Will Ness Apr 03 '13 at 07:52

1 Answers1

7

"it only needs to know that Phenotypes can produce the Genome which produced it "

this means that Phenotype is really a relation on two types, the other being a Genome type used to produce a given Phenotype:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

import Data.List (sortBy)

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) => a -> b

class (Eq a, Show a, Genome b) => Phenotype a b | a -> b where
    --  In case of Coevolution where each phenotype needs to be compared to 
    --  every other in the population
    fitness         :: [a] -> a -> Int 
    genome          :: a -> b

breed :: (Phenotype b a, 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

-- |Full generational replacement selection protocol
fullGenerational :: (Phenotype b a, Genome a) =>
    (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

fit pop a b = LT   -- dummy function

This compiles. Each Phenotype will have to provide exactly one implementation of genome.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • This does seem to be what I'm looking for. The only thing missing from this answer is the link you provided in the comments before(http://www.haskell.org/haskellwiki/Functional_dependencies) – Nordmoen Apr 02 '13 at 17:35
  • @Nordmoen btw I didn't remember the exact spelling for the language pragmas that are used here, by heart; I just tried to load the file into GHCi and it told me which options were missing. – Will Ness Apr 02 '13 at 18:12
  • @Nordmoen BTW with the `Genome b` constraint in Phenotype class definition, the constraint `Genome a` in both functions is redundant and can be removed. But it doesn't hurt, and perhaps serves additional documentational purpose. – Will Ness Apr 03 '13 at 08:03