Question

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

Was it helpful?

Solution

"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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top