So, I understand algebraic types and type classes very well, but I'm interested in the software-engineering/best practices side of it.
What is the modern consensus, if any, on typeclasses? Are they evil? Are they handy? Should they be used, and when?
Here's my case-study. I'm writing an RTS-style game, and I have different kinds of "units" (tank, scout, etc.). Say I want to get the max health of each unit. My two thoughts on how to define their types are as follows:
Different constructors of an ADT:
data Unit = Scout ... | Tank ...
maxHealth :: Unit -> Int
maxHealth Scout = 10
maxHealth Tank = 20
Typeclass for Unit, each kind is an instance
class Unit a where
maxHealth :: a -> Int
instance Unit Scout where
maxHealth scout = 10
instance Unit Tank where
maxHealth tank = 20
Obviously, there is going to be many more fields and functions in the final product. (For example, each unit will have a different position, etc. so not all of the functions will be constant).
The trick is, there might be some functions that make sense for some units, but not others. For example, every unit will have a getPosition function, but a tank might have a getArmour function, which doesn't make sense for a scout without armour.
Which is the "generally accepted" way to write this if I want other Haskellers to be able to understand and follow my code?
Type classes are a powerful tool used in functional programming to enable ad-hoc polymorphism, more commonly known as overloading.
Type families are parametric types that can be assigned specialized representations based on the type parameters they are instantiated with. They are the data type analogue of type classes: families are used to define overloaded data in the same way that classes are used to define overloaded functions.
In Haskell, type classes provide a structured way to control ad hoc polymorphism, or overloading. [For the stylistic reason we discussed in Section 3.1, we have chosen to define elem in infix form. == and || are the infix operators for equality and logical or, respectively.]
Haskell isn't an object-oriented language. All of the functionality built here from scratch already exists in a much more powerful form, using Haskell's type system.
Most Haskell programmers frown on needless typeclasses. These hurt type inference; you can't even make a list of Unit
s without tricks; in GHC, there's all the secret dictionary passing; they somehow make the Haddocks harder to read; they can lead to brittle hierarchies ... maybe others can give you further reasons. I guess a good rule would be to use them when it's much more painful to avoid them. For instance, without Eq
, you'd have to manually pass around the functions to compare, say, two [[[Int]]]
s (or use some ad-hoc runtime tests), which is one of the pain points of ML programming.
Take a look at this blog post. Your first method of using a sum type is OK, but if you want to allow users to mod the game with new units or whatever, I'd suggest something like
data Unit = Unit { name :: String, maxHealth :: Int }
scout, tank :: Unit
scout = Unit { name = "scout", maxHealth = 10 }
tank = Unit { name = "tank", maxHealth = 20 }
allUnits = [ scout
, tank
, Unit { name = "another unit", maxHealth = 5 }
]
In your example, you need to encode somewhere that a tank has armor but a scout doesn't. The obvious possibility is to augment the Unit type with extra information like a Maybe Armor
field or a list of special powers ... there's not necessarily a definitive way.
One heavyweight solution, probably overkill, is to use a library like Vinyl that provides extensible records, giving you a form of subtyping.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With