Reputation: 845
I would like to use haskell to implement a game, and would like to use a system of type classes to implement the item system. It would work something like this:
data Wood = Wood Int
instance Item Wood where
image a = "wood.png"
displayName a = "Wood"
instance Flammable Wood where
burn (Wood health) | health' <= 0 = Ash
| otherwise = Wood health'
where health' = health - 100
where the Item and Flammable classes are something like this:
class Item a where
image :: a -> String
displayName :: a -> String
class Flammable a where
burn :: (Item b) => a -> b
To do this, I would need a way to detect whether a value is an instance of a type class.
The Data.Data module gives a similar functionality so that leads me to believe that this is possible.
Upvotes: 5
Views: 399
Reputation: 53665
Here's the problem:
burn :: (Item b) => a -> b
What this means is that the result value of burn
must be polymorphic. It must be able to fill any hole for any instance of Item
.
Now, it's quite apparent you're trying to write something like this (in imaginary OO language with interfaces and subclassing):
Interface Item {
String getImage();
String getDisplayName();
}
Interface Flammable {
Item burn();
}
In this sort of code, you're saying that burn
will produce some item, without any guarantees about what kind of item it is. This is the difference between "for all" and "there exists". What you wanted to express in the Haskell code was "there exists", but what you actually expressed was "for all".
Now if you're really sure you want to do "there exists" functionality, you can take a look at using Existential Types. But beware. If you are planning on writing code like this:
if (foo instanceof Flammable) {
...
}
Then you are almost certainly doing it wrong, and will run into much pain and agony. Instead consider hammar's suggested alternatives.
Upvotes: 5
Reputation: 139830
Type classes are probably the wrong way to go here. Consider using plain algebraic data types instead, for example:
data Item = Wood Int | Ash
image Wood = "wood.png"
image Ash = ...
displayName Wood = "Wood"
displayName Ash = "Ash"
burn :: Item -> Maybe Item
burn (Wood health) | health' <= 0 = Just Ash
| otherwise = Just (Wood health')
where health' = health - 100
burn _ = Nothing -- Not flammable
If this makes it too hard to add new items, you can instead encode the operations in the data type itself.
data Item = Item { image :: String, displayName :: String, burn :: Maybe Item }
ash :: Item
ash = Item { image = "...", displayName = "Ash", burn :: Nothing }
wood :: Int -> Item
wood health = Item { image = "wood.png", displayName = "Wood", burn = Just burned }
where burned | health' <= 0 = ash
| otherwise = wood health'
health' = health - 100
However, this makes it harder to add new functions. The problem of doing both at the same time is known as the expression problem. There is a nice lecture on Channel 9 by Dr. Ralf Lämmel where he explains this problem more in depth and discusses various non-solutions, well worth the watch if you have time.
There are approaches to solving it, but they are considerably more complex than the two designs I've illustrated, so I recommend using one of those if it fits your needs, and not worrying about the expression problem unless you have to.
Upvotes: 8