Learn You a Haskell for Great Good! - Miran Lipovaca [114]
newtype CharList = CharList { getCharList :: [Char] }
We can’t use ++ to put together a CharList and a list of type [Char]. We can’t even use ++ to put together two CharList lists, because ++ works only on lists, and the CharList type isn’t a list, even though it could be said that CharList contains a list. We can, however, convert two CharLists to lists, ++ them, and then convert that back to a CharList.
When we use record syntax in our newtype declarations, we get functions for converting between the new type and the original type—namely the value constructor of our newtype and the function for extracting the value in its field. The new type also isn’t automatically made an instance of the type classes that the original type belongs to, so we need to derive or manually write it.
In practice, you can think of newtype declarations as data declarations that can have only one constructor and one field. If you catch yourself writing such a data declaration, consider using newtype.
The data keyword is for making your own data types. You can go hog wild with them. They can have as many constructors and fields as you wish and can be used to implement any algebraic data type—everything from lists and Maybe-like types to trees.
In summary, use the keywords as follows:
If you just want your type signatures to look cleaner and be more descriptive, you probably want type synonyms.
If you want to take an existing type and wrap it in a new type in order to make it an instance of a type class, chances are you’re looking for a newtype.
If you want to make something completely new, odds are good that you’re looking for the data keyword.
About Those Monoids
Type classes in Haskell are used to present an interface for types that have some behavior in common. We started out with simple type classes like Eq, which is for types whose values can be equated, and Ord, which is for things that can be put in an order. Then we moved on to more interesting type classes, like Functor and Applicative.
When we make a type, we think about which behaviors it supports (what it can act like) and then decide which type classes to make it an instance of based on the behavior we want. If it makes sense for values of our type to be equated, we make our type an instance of the Eq type class. If we see that our type is some kind of functor, we make it an instance of Functor, and so on.
Now consider the following: * is a function that takes two numbers and multiplies them. If we multiply some number with a 1, the result is always equal to that number. It doesn’t matter if we do 1 * x or x * 1— the result is always x. Similarly, ++ is a function that takes two things and returns a third. But instead of multiplying numbers, it takes two lists and concatenates them. And much like *, it also has a certain value that doesn’t change the other one when used with ++. That value is the empty list: [].
ghci> 4 * 1
4
ghci> 1 * 9
9
ghci> [1,2,3] ++ []
[1,2,3]
ghci> [] ++ [0.5, 2.5]
[0.5,2.5]
It seems that * together with 1 and ++ along with [] share some common properties:
The function takes two parameters.
The parameters and the returned value have the same type.
There exists such a value that doesn’t change other values when used with the binary function.
There’s another thing that these two operations have in common that may not be as obvious as our previous observations: When we have three or more values and we want to use the binary function to reduce them to a single result, the order in which we apply the binary function to the values doesn’t matter. For example, whether we use (3 * 4) * 5 or 3 * (4 * 5), the result is 60. The same goes for ++:
ghci> (3 * 2) * (8 * 5)
240
ghci> 3 * (2 * (8 * 5))
240
ghci> "la" ++ ("di" ++ "da")
"ladida"
ghci> ("la" ++ "di") ++ "da"
"ladida"
We call this property associativity. * is associative, and so is ++. However, -, for example, is not associative; the expressions (5