The two following pieces of code seem really similar. However there must be some differences and I am hoping that someone could point them out.
data Animal = Cat | Dog
speak :: Animal -> String
speak Cat = "meowh"
speak Dog = "wouf"
and
data Animal = Animal { speak :: String }
cat = Animal { speak = "meowh"}
dog = Animal { speak = "wouf" }
Good question! You've struck at the heart of one of the fundamental problems of software engineering. Per Wadler, it's called the Expression Problem, and, roughly summarised, the question is:
Should it be easy to add new operations, or new types of data?
Your first example makes it easy to add new operations to existing animals. We can process Animal
values in all sorts of ways without changing the definition of Animal
:
numberOfTeeth :: Animal -> Int
numberOfTeeth Cat = 30
numberOfTeeth Dog = 42
food :: Animal -> String
food Cat = "Fish"
food Dog = "Sausages" -- probably stolen from a cartoon butcher
The downside is that it's hard to add new types of animal. You have to add new constructors to Animal
and change all the existing operations:
data Animal = Cat | Dog | Crocodile
speak :: Animal -> String
speak Cat = "miaow"
speak Dog = "woof"
speak Crocodile = "RAWR"
numberOfTeeth :: Animal -> Int
numberOfTeeth Cat = 30
numberOfTeeth Dog = 42
numberOfTeeth Crocodile = 100000 -- I'm not a vet
food :: Animal -> String
food Cat = "Fish"
food Dog = "Sausages"
food Crocodile = "Human flesh"
Your second example flips the matrix, making it easy to add new types,
crocodile = Animal { speak = "RAWR" }
but hard to add new functions - it means adding new fields to Animal
and updating all the existing animals.
data Animal = Animal {
speak :: String,
numberOfTeeth :: Int,
food :: String
}
cat = Animal {
speak = "miaow",
numberOfTeeth = 30,
food = "Fish"
}
dog = Animal {
speak = "woof",
numberOfTeeth = 42,
food = "Sausages"
}
crocodile = Animal {
speak = "RAWR",
numberOfTeeth = 100000,
food = "Human flesh"
}
Don't underestimate how big of a deal the Expression Problem is! If you're working on a published library, you may find yourself contending with operations or types defined by someone you've never met in a codebase you can't change. You have to think carefully about the way you expect people to use your system, and decide how to orient the design of your library.
Over the years, smart people have invented lots of clever ways of solving the Expression Problem, to support new operations and new types. These solutions tend to be complicated, using the most advanced features of the most modern programming languages. In the real world, this is just another trade-off engineers have to consider - is solving the Expression Problem worth the code-complexity it'll cause?
First, let's assign the types the names, which closer reflect their essence:
data AnimalType =
Cat | Dog
newtype Phrase =
Phrase { phrase :: String }
It already becomes evident that they are very different and clearly isolatable.
A phrase here is a way more general thing. It can be spoken not just by animals, but by robots at least as well. But more so it can not only be spoken, but also be transformed with operations like upper-casing and etc. Such operations would make no sense for the AnimalType
type.
AnimalType
OTOH has its own benefits. By matching the type, you can pick a type of food the animal needs or its size and etc. You can't do that on Phrase
.
You can also have both types coexist in isolation in your application and have a transformation (and, hence, a dependency) from the more specific to the more general one:
module Animal where
import qualified Phrase
speakPhrase :: Animal -> Phrase.Phrase
speakPhrase =
Phrase.Phrase . speak
What's causing your confusion is that your problem lacks the context of an application. Once you'll provide it to yourself, you'll get the info on what you actually need this thing to do and which data it'll operate on.
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