Preface
When I started learning haskell I was immediately overwhelmed. I dived heads first into using it for an actual work project and I discovered that most real world libraries used language extensions available only in GHC. That let me down a bit at first, after all, who wants to use a language so lacking that you need extensions specific to one vendor in order to actually use it, right?
Well, I got back on my horse and decided to learn about all this extensions and I deduced there were 3 hot topics in the Haskell community which apparently addressed the same kind of problems: GADT's, TypeFamilies and Functional Dependencies. Trying to look up for resources to learn about them I could only find articles describing what they were, and teaching how to use them. But none of them actually explained how we came to need them! So I decided to write this tutorial, using a friendly example, to try and explain why we need Type Families.
Ok let's do this.
Have you heard about Pokemon? They are these wonderful creatures that inhabit the Pokemon world. You could say they're like animals, with extraordinary abilities. All Pokemon have a type, and their abilities depend on their type. For example, Pokemon of the Fire type can breathe fire, while Pokemon of the Water type can shoot water beams.
Pokemon are owned by people, and their special abilities may be put to good use on productive endeavors, but some people just put their Pokemon to fight with other people's Pokemon. These people call themselves Pokemon Trainers. All of this may sound borderline animal cruelty at first, but it's quite fun and everybody seems to be OK with it, including Pokemon. Bear in mind people in the Pokemon world also seem to be OK with 10 year olds leaving home to risk their lives in order to become the very best Pokemon trainer, like no one ever was.
We're going to use Haskell to represent a restricted (and somewhat altered, with pardon from the fans) portion of the Pokemon world, such that:
- Pokemon have a type, in our case restricted to Fire, Water or Grass.
- There are 3 Pokemon of each type: Charmander, Charmeleon and Charizard are Fire type. Squirtle, Wartortle and Blastoise are Water type. And finally, Bulbasaur, Ivysaur and Venusaur are Grass type.
- Each type has its own distinctive abilities, called moves: Water types perform Water moves, Fire types perform Fire moves, and Grass types perform Grass moves.
- When battling: Fire Pokemon always beat Grass Pokemon, Grass Pokemon always beat Water Pokemon, and Water Pokemon always beat Fire Pokemon.
- We never battle 2 pokemon of the same type, ever, because we wouldn't be able to decide who the winner is anyways.
- Other people can add their own pokemon in other modules.
- The typechecker helps us enforcing this rules strictly.
First Attempt
Initially, we're going to try to implement these rules without using Type Clases and Type families.
We start with some types for our Pokemon and their distinctive moves, we keep them separate because that's going to help us know which moves go with which pokemon types. And for that purpose, we define the functions for each pokemon to pick their move.
data Fire = Charmander | Charmeleon | Charizard deriving Show -- These are actual Pokemon names
data Water = Squirtle | Wartortle | Blastoise deriving Show
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
data FireMove = Ember | FlameThrower | FireBlast deriving Show -- These are actual Pokemon moves
data WaterMove = Bubble | WaterGun deriving Show
data GrassMove = VineWhip deriving Show
pickFireMove :: Fire -> FireMove
pickFireMove Charmander = Ember
pickFireMove Charmeleon = FlameThrower
pickFireMove Charizard = FireBlast
pickWaterMove :: Water -> WaterMove
pickWaterMove Squirtle = Bubble
pickWaterMove _ = WaterGun
pickGrassMove :: Grass -> GrassMove
pickGrassMove _ = VineWhip
So far so good, the type checker will make sure we can only create the right pokemon and they can only use the special abilities that go with their type.
Now we should make them battle. Our battles visual representation will show each pokemon with the move they used and the winner, like this:
printBattle :: String -> String -> String -> String -> String -> IO ()
printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do
putStrLn $ pokemonOne ++ " used " ++ moveOne
putStrLn $ pokemonTwo ++ " used " ++ moveTwo
putStrLn $ "Winner is: " ++ winner ++ "\n"
main :: IO ()
main =
printBattle "Water Pokemon" "Water Attack" "Fire Pokemon" "Fire Attack" "Water Pokemon"
Displaying the move is just for showing, we will decide the winner based on the pokemon type regardless of the move they used. Here's an example of a function to fight between Water and Fire types.
battleWaterVsFire :: Water -> Fire -> IO ()
battleWaterVsFire water fire = do
printBattle (show water) moveOne (show fire) moveTwo (show water)
where
moveOne = show $ pickWaterMove water
moveTwo = show $ pickFireMove fire
battleFireVsWater = flip battleWaterVsFire -- Same as above, but with flipped arguments
Now we mix it all together, define the remaining battle functions and we have a program!
data Fire = Charmander | Charmeleon | Charizard deriving Show
data Water = Squirtle | Wartortle | Blastoise deriving Show
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
data FireMove = Ember | FlameThrower | FireBlast deriving Show
data WaterMove = Bubble | WaterGun deriving Show
data GrassMove = VineWhip deriving Show
pickFireMove :: Fire -> FireMove
pickFireMove Charmander = Ember
pickFireMove Charmeleon = FlameThrower
pickFireMove Charizard = FireBlast
pickWaterMove :: Water -> WaterMove
pickWaterMove Squirtle = Bubble
pickWaterMove _ = WaterGun
pickGrassMove :: Grass -> GrassMove
pickGrassMove _ = VineWhip
printBattle :: String -> String -> String -> String -> String -> IO ()
printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do
putStrLn $ pokemonOne ++ " used " ++ moveOne
putStrLn $ pokemonTwo ++ " used " ++ moveTwo
putStrLn $ "Winner is: " ++ winner ++ "\n"
-- show The battle functions
battleWaterVsFire :: Water -> Fire -> IO ()
battleWaterVsFire water fire = do
printBattle (show water) moveOne (show fire) moveTwo (show water)
where
moveOne = show $ pickWaterMove water
moveTwo = show $ pickFireMove fire
battleFireVsWater = flip battleWaterVsFire
battleGrassVsWater :: Grass -> Water -> IO ()
battleGrassVsWater grass water = do
printBattle (show grass) moveOne (show water) moveTwo (show grass)
where
moveOne = show $ pickGrassMove grass
moveTwo = show $ pickWaterMove water
battleWaterVsGrass = flip battleGrassVsWater
battleFireVsGrass :: Fire -> Grass -> IO ()
battleFireVsGrass fire grass = do
printBattle (show fire) moveOne (show grass) moveTwo (show fire)
where
moveOne = show $ pickFireMove fire
moveTwo = show $ pickGrassMove grass
battleGrassVsFire = flip battleFireVsGrass
main :: IO ()
main = do
battleWaterVsFire Squirtle Charmander
battleFireVsWater Charmeleon Wartortle
battleGrassVsWater Bulbasaur Blastoise
battleWaterVsGrass Wartortle Ivysaur
battleFireVsGrass Charmeleon Ivysaur
battleGrassVsFire Venusaur Charizard
-- /show
Introducing Type Classes
Oh my, that was repetitive: Imagine someone adding an Electric type pokemon like Pikachu into the mix, they would need to define their own battleElectricVs(Grass|Fire|Water) functions. There are some patterns emerging here, which we may formalize to help people gain a better understanding of what pokemon are and help them create new ones.
Here's what we've learned:
- Pokemon use function to pick their move.
- Battles decide a winner and print a description of the battle.
We will define some type classes to formalize those, and while we're at it we will also address the funky naming scheme where each function includes the types it operates on.
At this point, I assume you're familiar with traditional type classes, if not, go ahead and read this chapter of the "Learn You a Haskell for Great Good Book"
The Pokemon Type Class
The Pokemon Type Class will represent the knowledge that pokemon pick their move. It lets us define pickMove to be overloaded, so that the same function can operate on different types for which the Type Class has been defined.
Contrary to 'vanilla' Type Classes, our Pokemon class will need to know about 2 types: The type of pokemon and the type of move to use, since the later will depend on the former. We need to enable a language extension in order to pass these 2 paramters to our Type Class: MultiParamTypeClasses.
Also notice we had to add constraints such that the pokemon type and move types are also 'showable'.
Here's the definition, along with some instances for the existing types of pokemon.
{-# LANGUAGE MultiParamTypeClasses #-}
data Fire = Charmander | Charmeleon | Charizard deriving Show
data Water = Squirtle | Wartortle | Blastoise deriving Show
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
data FireMove = Ember | FlameThrower | FireBlast deriving Show
data WaterMove = Bubble | WaterGun deriving Show
data GrassMove = VineWhip deriving Show
-- show
class (Show pokemon, Show move) => Pokemon pokemon move where
pickMove :: pokemon -> move
instance Pokemon Fire FireMove where
pickMove Charmander = Ember
pickMove Charmeleon = FlameThrower
pickMove Charizard = FireBlast
instance Pokemon Water WaterMove where
pickMove Squirtle = Bubble
pickMove _ = WaterGun
instance Pokemon Grass GrassMove where
pickMove _ = VineWhip
main :: IO ()
main = do
print (pickMove Charmander :: FireMove)
print (pickMove Blastoise :: WaterMove)
print (pickMove Bulbasaur :: GrassMove)
-- show /
Notice how things start to get hairy: Since the Pokemon type and the Move type are handled as separate parameters in the Type Class, calling pickMove and just passing in a Charmander makes the type checker lookup an instance of the Pokemon Type Class which looks like Pokemon Fire a. Which we don't have, so it fails.
Try calling pickMove above without the type signatures, and look at the error that comes up.
By saying that we want pickMove to produce a FireMove we give the typechecker all the information it needs to decide to use the Pokemon Fire FireMove instance.
The Battle type class
We already have pokemon who can pick their moves, now we need an abstraction that represents pokemon can fight each other, to replace the battle*Vs* family of functions.
So next we will be defining another MultiParamTypeClass, which will constrain it's arguments to also be Pokemon, which is already a MultiParamTypeClass. We will also define the instances for each type of battle we want to support.
{-# LANGUAGE MultiParamTypeClasses #-}
data Fire = Charmander | Charmeleon | Charizard deriving Show
data Water = Squirtle | Wartortle | Blastoise deriving Show
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
data FireMove = Ember | FlameThrower | FireBlast deriving Show
data WaterMove = Bubble | WaterGun deriving Show
data GrassMove = VineWhip deriving Show
class (Show pokemon, Show move) => Pokemon pokemon move where
pickMove :: pokemon -> move
instance Pokemon Fire FireMove where
pickMove Charmander = Ember
pickMove Charmeleon = FlameThrower
pickMove Charizard = FireBlast
instance Pokemon Water WaterMove where
pickMove Squirtle = Bubble
pickMove _ = WaterGun
instance Pokemon Grass GrassMove where
pickMove _ = VineWhip
printBattle :: String -> String -> String -> String -> String -> IO ()
printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do
putStrLn $ pokemonOne ++ " used " ++ moveOne
putStrLn $ pokemonTwo ++ " used " ++ moveTwo
putStrLn $ "Winner is: " ++ winner ++ "\n"
-- show The battle Type Class
class (Pokemon pokemon move, Pokemon foe foeMove) => Battle pokemon move foe foeMove where
battle :: pokemon -> foe -> IO ()
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)
where
move = pickMove pokemon
foeMove = pickMove foe
instance Battle Water WaterMove Fire FireMove
main :: IO ()
main = do
battle Squirtle Charmander
-- show /
When we run the snippet above we get an error, the Type Checker is telling us that in order to battle a Squirtle and a Charmander we need to have an instance of our Battle typeclass that works like "Battle Water move0 Fire foeMove0".
This brings us back to the issue we faced just a moment ago, when we defined the Pokemon MultiParameterTypeClass. In that case we solved it by giving a signature to the pickMove call.
Since the return type for battle is IO () we're not as lucky this time.
A quick and extremely awful fix to this problem is making battle return the moves used, so that we can add a type signature whenever we call 'battle' to help the typechecker decide the instances used, so we will do that, for now :)
I'll go ahead and define battle to return an IO (move, foeMove). I'll also define all the remaining instances to end up with the same functionality we had on the first iteration, only that now everything should be a bit more formalized.
{-# LANGUAGE MultiParamTypeClasses #-}
import Data.Tuple (swap)
data Fire = Charmander | Charmeleon | Charizard deriving Show
data Water = Squirtle | Wartortle | Blastoise deriving Show
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
data FireMove = Ember | FlameThrower | FireBlast deriving Show
data WaterMove = Bubble | WaterGun deriving Show
data GrassMove = VineWhip deriving Show
class (Show pokemon, Show move) => Pokemon pokemon move where
pickMove :: pokemon -> move
instance Pokemon Fire FireMove where
pickMove Charmander = Ember
pickMove Charmeleon = FlameThrower
pickMove Charizard = FireBlast
instance Pokemon Water WaterMove where
pickMove Squirtle = Bubble
pickMove _ = WaterGun
instance Pokemon Grass GrassMove where
pickMove _ = VineWhip
printBattle :: String -> String -> String -> String -> String -> IO ()
printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do
putStrLn $ pokemonOne ++ " used " ++ moveOne
putStrLn $ pokemonTwo ++ " used " ++ moveTwo
putStrLn $ "Winner is: " ++ winner ++ "\n"
-- show Our Battle Type Class, yuck
class (Pokemon pokemon move, Pokemon foe foeMove)
=> Battle pokemon move foe foeMove where
battle :: pokemon -> foe -> IO (move, foeMove)
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)
return (move, foeMove)
where
foeMove = pickMove foe
move = pickMove pokemon
instance Battle Water WaterMove Fire FireMove
instance Battle Fire FireMove Water WaterMove where
battle a b = fmap swap $ flip battle a b
instance Battle Grass GrassMove Water WaterMove
instance Battle Water WaterMove Grass GrassMove where
battle a b = fmap swap $ flip battle a b
instance Battle Fire FireMove Grass GrassMove
instance Battle Grass GrassMove Fire FireMove where
battle a b = fmap swap $ flip battle a b
main :: IO ()
main = do
battle Squirtle Charmander :: IO (WaterMove, FireMove)
battle Charmeleon Wartortle :: IO (FireMove, WaterMove)
battle Bulbasaur Blastoise :: IO (GrassMove, WaterMove)
battle Wartortle Ivysaur :: IO (WaterMove, GrassMove)
battle Charmeleon Ivysaur :: IO (FireMove, GrassMove)
battle Venusaur Charizard :: IO (GrassMove, FireMove)
putStrLn "Done Fighting"
-- show /
Introducing Type Families, finally!
So, our program so far kinda sucks. We have to carry around all these type signatures, and we even had to change the inner behaviour of one of our fuctions (battle) just so that we could use a type signature to help the compiler. I would go as far as to say that the current iteration of this program, while more formal and less repetitive, it's not such a big improvement after all the new uglyness we introduced.
But we can track down the uglyness to our Pokemon Type Class declaration. It has the type of pokemon and the type of move as two separate class variables: The typechecker doesn't know there is a relation between the pokemon type and the moves it can use. It will even allow us to define Pokemon instances where Water pokemon perform Fire moves! That's not right, but you could, go back and try creating an instance for Pokemon Fire WaterMove.
That's where type families come into play: They let us tell the typechecker that Fire type pokemon go with FireMove moves and so on.
The Pokemon type class using type families.
In order to use Type Families we will need to enable the TypeFamilies extension. Once we do, our Pokemon type class looks as following:
class (Show a, Show (Move a)) => Pokemon a where
data Move a :: *
pickMove :: a -> Move a
We declare our Pokemon Type Class to have an argument, and an associated Move type. Move becomes a 'type function', returning the type of move to be used. This means that instead of 'FireMove' we will now have 'Move Fire', instead of 'WaterMove' we can have 'Move Water' and so on.
Notice how the constraint looks similar to the previous one, only that instead of 'Show move' we use 'Show (Move a))'. We need to enable yet another extension to be able to do this: FlexibleContexts.
Then, Haskell provides this nice sintax sugar so that we can define the actual associated data type constructors right where we define our instances.
Let's redefine all of our data types, our pokemon type class and all the required instances using Type Families:
{-# LANGUAGE TypeFamilies, FlexibleContexts #-}
class (Show pokemon, Show (Move pokemon)) => Pokemon pokemon where
data Move pokemon :: *
pickMove :: pokemon -> Move pokemon
data Fire = Charmander | Charmeleon | Charizard deriving Show
instance Pokemon Fire where
data Move Fire = Ember | FlameThrower | FireBlast deriving Show
pickMove Charmander = Ember
pickMove Charmeleon = FlameThrower
pickMove Charizard = FireBlast
data Water = Squirtle | Wartortle | Blastoise deriving Show
instance Pokemon Water where
data Move Water = Bubble | WaterGun deriving Show
pickMove Squirtle = Bubble
pickMove _ = WaterGun
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
instance Pokemon Grass where
data Move Grass = VineWhip deriving Show
pickMove _ = VineWhip
main :: IO ()
main = do
print $ pickMove Squirtle
print $ pickMove Charmander
print $ pickMove Ivysaur
That's pretty neat, right? We don't have to put any type signatures for pickMove to work! But don't scroll up just yet: Wait to see the full program on this third iteration and compare it to the second iteration for the full effect.
The new Battle type class
So, now that we don't need those verbose type signatures, we can revert that ugly hack we introduced and go back to a version of our 'battle' function that just returns IO ().
class (Pokemon pokemon, Pokemon foe) => Battle pokemon foe where
battle :: pokemon -> foe -> IO ()
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)
where
foeMove = pickMove foe
move = pickMove pokemon
Also, notice how Battle does not need to know about moves anymore, it's back to just battling pokemon pretty much like the naive implementation in our first iteration.
Let me go ahead again and define all the remaining instances for battle and give you the full program in it's third iteration:
{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, FlexibleContexts #-}
class (Show pokemon, Show (Move pokemon)) => Pokemon pokemon where
data Move pokemon :: *
pickMove :: pokemon -> Move pokemon
data Fire = Charmander | Charmeleon | Charizard deriving Show
instance Pokemon Fire where
data Move Fire = Ember | FlameThrower | FireBlast deriving Show
pickMove Charmander = Ember
pickMove Charmeleon = FlameThrower
pickMove Charizard = FireBlast
data Water = Squirtle | Wartortle | Blastoise deriving Show
instance Pokemon Water where
data Move Water = Bubble | WaterGun deriving Show
pickMove Squirtle = Bubble
pickMove _ = WaterGun
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
instance Pokemon Grass where
data Move Grass = VineWhip deriving Show
pickMove _ = VineWhip
printBattle :: String -> String -> String -> String -> String -> IO ()
printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do
putStrLn $ pokemonOne ++ " used " ++ moveOne
putStrLn $ pokemonTwo ++ " used " ++ moveTwo
putStrLn $ "Winner is: " ++ winner ++ "\n"
class (Pokemon pokemon, Pokemon foe) => Battle pokemon foe where
battle :: pokemon -> foe -> IO ()
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)
where
foeMove = pickMove foe
move = pickMove pokemon
instance Battle Water Fire
instance Battle Fire Water where
battle = flip battle
instance Battle Grass Water
instance Battle Water Grass where
battle = flip battle
instance Battle Fire Grass
instance Battle Grass Fire where
battle = flip battle
main :: IO ()
main = do
battle Squirtle Charmander
battle Charmeleon Wartortle
battle Bulbasaur Blastoise
battle Wartortle Ivysaur
battle Charmeleon Ivysaur
battle Venusaur Charizard
So that's it. Our program finally looks decent, we've improved it to the point were now we type check more, repeat less and have a clean API to offer to other developers.
Cool! We're done here! Hope you liked it!
Ok ok, I get it, you're having fun and you can't believe it's over so soon, besides you've looked at your browser's scroll bar and it still shows there's a bit more page to look at below.
So, let's add a feature to our Pokemon world:
We're currently defining Battle instances for the Water and Fire types as 'Battle Water Fire', and then we define 'Battle Water Fire' to be the same as the previous one with flipped arguments. The first pokemon passed to battle is always the winner, and the output is always as follows:
-- Winner Pokemon move
-- Loser Pokemon move
-- Winner pokemon Wins.
Even when the instance has the loser first, the first line of output is always the winner's attack.
But let's change that and make it possible for Battle instances to decide who's the winner of the match, so that in some cases the output of the battle ends up being:
-- Loser Pokemon move
-- Winner Pokemon move
-- Winner pokemon Wins
Associated Type Synonyms
When deciding to return Either of two things, you would regularly return an 'Either a b', but that's runtime, and we want the typechecker to make sure than when a Fire and a Water battle, the Water is always the winner.
So we define a new function in Battle called 'winner', which will receive the 2 contendants in the same order as they were passed to the battle function, and will decide who's the winner.
But returning either of the input pokemon yields a complication in the 'winner' function, have a look for yourself:
class (Pokemon pokemon, Pokemon foe) => Battle pokemon foe where
battle :: pokemon -> foe -> IO ()
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)
where
foeMove = pickMove foe
move = pickMove pokemon
winner :: pokemon -> foe -> ??? -- Is it 'pokemon' or 'foe'?
instance Battle Water Fire where
winner :: Water -> Fire -> Water -- Water is the first type variable of the type class, namely: pokemon
winner water _ = water
instance Battle Fire Water
winner :: Fire -> Water -> Water -- Water is the second type variable of the type class, namely: foe
winner _ water = water
See, for 'Battle Water Fire' instances the return type of 'winner' will be the same as the 'pokemon' type variable mentioned in the Type Class, while for 'Battle Fire Water' it's going to be 'foe'
Fortunately, type families also include support for associated type synonyms: On the Battle Type Class we define that we're going to have a "Winner pokemon foo" type, and it's up to instances to decide which type that would be.
We use 'type' and not 'data' because it'll be an alias to either 'pokemon' or 'foe'.
'Winner' alone is a type function with kind signature * -> * -> *, which will receive both 'pokemon' and 'foo' and return which one to use.
We also define a default implementation for Winner, which chooses 'pokemon' when given 'pokemon' and 'foo'.
class (Show (Winner pokemon foe), Pokemon pokemon, Pokemon foe) => Battle pokemon foe where
type Winner pokemon foe :: * -- this is the associated type
type Winner pokemon foe = pokemon -- this is the default implementation
battle :: pokemon -> foe -> IO ()
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show winner)
where
foeMove = pickMove foe
move = pickMove pokemon
winner = pickWinner pokemon foe
pickWinner :: pokemon -> foe -> (Winner pokemon foe)
So here it is, the final pokemon program where sometimes the foe wins the battle:
{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, FlexibleContexts #-}
class (Show pokemon, Show (Move pokemon)) => Pokemon pokemon where
data Move pokemon :: *
pickMove :: pokemon -> Move pokemon
data Fire = Charmander | Charmeleon | Charizard deriving Show
instance Pokemon Fire where
data Move Fire = Ember | FlameThrower | FireBlast deriving Show
pickMove Charmander = Ember
pickMove Charmeleon = FlameThrower
pickMove Charizard = FireBlast
data Water = Squirtle | Wartortle | Blastoise deriving Show
instance Pokemon Water where
data Move Water = Bubble | WaterGun deriving Show
pickMove Squirtle = Bubble
pickMove _ = WaterGun
data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show
instance Pokemon Grass where
data Move Grass = VineWhip deriving Show
pickMove _ = VineWhip
printBattle :: String -> String -> String -> String -> String -> IO ()
printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do
putStrLn $ pokemonOne ++ " used " ++ moveOne
putStrLn $ pokemonTwo ++ " used " ++ moveTwo
putStrLn $ "Winner is: " ++ winner ++ "\n"
class (Show (Winner pokemon foe), Pokemon pokemon, Pokemon foe) => Battle pokemon foe where
type Winner pokemon foe :: *
type Winner pokemon foe = pokemon
battle :: pokemon -> foe -> IO ()
battle pokemon foe = do
printBattle (show pokemon) (show move) (show foe) (show foeMove) (show winner)
where
foeMove = pickMove foe
move = pickMove pokemon
winner = pickWinner pokemon foe
pickWinner :: pokemon -> foe -> (Winner pokemon foe)
instance Battle Water Fire where
pickWinner pokemon foe = pokemon
instance Battle Fire Water where
type Winner Fire Water = Water
pickWinner = flip pickWinner
instance Battle Grass Water where
pickWinner pokemon foe = pokemon
instance Battle Water Grass where
type Winner Water Grass = Grass
pickWinner = flip pickWinner
instance Battle Fire Grass where
pickWinner pokemon foe = pokemon
instance Battle Grass Fire where
type Winner Grass Fire = Fire
pickWinner = flip pickWinner
main :: IO ()
main = do
battle Squirtle Charmander
battle Charmeleon Wartortle
battle Bulbasaur Blastoise
battle Wartortle Ivysaur
battle Charmeleon Ivysaur
battle Venusaur Charizard
That's it, hope it clicked! Try adding your own Electric Pokemon at the end of the interactive snippet above!