Redesign of Haskell type classes
Asked Answered
Y

1

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

Yearling answered 2/4, 2013 at 14:34 Comment(4)
Translate the type class to the equivalent dictionary: see this and this.Unit
@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.Yearling
Sorry, I misunderstood what you meant by ambiguity. Ignore me.Unit
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! :)Carburetor
C
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.

Carburetor answered 2/4, 2013 at 17:12 Comment(3)
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(haskell.org/haskellwiki/Functional_dependencies)Yearling
@Yearling 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.Carburetor
@Yearling 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.Carburetor

© 2022 - 2024 — McMap. All rights reserved.