Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cases of types vs fields of records in Haskell

Tags:

haskell

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" }
like image 582
Ronan Avatar asked Aug 21 '16 20:08

Ronan


2 Answers

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?

like image 200
Benjamin Hodgson Avatar answered Nov 04 '22 22:11

Benjamin Hodgson


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.

like image 1
Nikita Volkov Avatar answered Nov 04 '22 23:11

Nikita Volkov