Haskell – 108 – functors, funtors applicativi e monoids – 7

Continuo da qui, copio qui, scollare fino a “On newtype laziness”

On newtype laziness
We mentioned that newtype is usually faster than data. The only thing that can be done with newtype is turning an existing type into a new type, so internally, Haskell can represent the values of types defined with newtype just like the original ones, only it has to keep in mind that the their types are now distinct. This fact means that not only is newtype faster, it’s also lazier. Let’s take a look at what this means.

Like we’ve said before, Haskell is lazy by default, which means that only when we try to actually print the results of our functions will any computation take place. Furthemore, only those computations that are necessary for our function to tell us the result will get carried out. The undefined value in Haskell represents an erronous computation. If we try to evaluate it (that is, force Haskell to actually compute it) by printing it to the terminal, Haskell will throw a hissy fit (technically referred to as an exception):

Prelude> undefined
*** Exception: Prelude.undefined

However, if we make a list that has some undefined values in it but request only the head of the list, which is not undefined, everything will go smoothly because Haskell doesn’t really need to evaluate any other elements in a list if we only want to see what the first element is:

Prelude> head [3,4,5,undefined,2,undefined]

Now consider the following type:

data CoolBool = CoolBool { getCoolBool :: Bool }

It’s your run-of-the-mill algebraic data type that was defined with the data keyword. It has one value constructor, which has one field whose type is Bool. Let’s make a function that pattern matches on a CoolBool and returns the value "hello" regardless of whether the Bool inside the CoolBool was True or False:


data CoolBool = CoolBool { getCoolBool :: Bool }

helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"

Instead of applying this function to a normal CoolBool, let’s throw it a curveball and apply it to undefined!

Prelude> :l hm
[1 of 1] Compiling Main             ( hm.hs, interpreted )
Ok, modules loaded: Main.
*Main> helloMe undefined
"*** Exception: Prelude.undefined

Yikes! An exception! Now why did this exception happen? Types defined with the data keyword can have multiple value constructors (even though CoolBool only has one). So in order to see if the value given to our function conforms to the (CoolBool _) pattern, Haskell has to evaluate the value just enough to see which value constructor was used when we made the value. And when we try to evaluate an undefined value, even a little, an exception is thrown.

Instead of using the data keyword for CoolBool, let’s try using newtype:

newtype CoolBool = CoolBool { getCoolBool :: Bool }

We don’t have to change our helloMe function, because the pattern matching syntax is the same if you use newtype or data to define your type. Let’s do the same thing here and apply helloMe to an undefined value:

*Main> :l hm-nt
[1 of 1] Compiling Main             ( hm-nt.hs, interpreted )
Ok, modules loaded: Main.
*Main> helloMe undefined

It worked! Hmmm, why is that? Well, like we’ve said, when we use newtype, Haskell can internally represent the values of the new type in the same way as the original values. It doesn’t have to add another box around them, it just has to be aware of the values being of different types. And because Haskell knows that types made with the newtype keyword can only have one constructor, it doesn’t have to evaluate the value passed to the function to make sure that it conforms to the (CoolBool _) pattern because newtype types can only have one possible value constructor and one field!

This difference in behavior may seem trivial, but it’s actually pretty important because it helps us realize that even though types defined with data and newtype behave similarly from the programmer’s point of view because they both have value constructors and fields, they are actually two different mechanisms. Whereas data can be used to make your own types from scratch, newtype is for making a completely new type out of an existing type. Pattern matching on newtype values isn’t like taking something out of a box (like it is with data), it’s more about making a direct conversion from one type to another.

type vs. newtype vs. data
At this point, you may be a bit confused about what exactly the difference between type, data and newtype is, so let’s refresh our memory a bit.

The type keyword is for making type synonyms. What that means is that we just give another name to an already existing type so that the type is easier to refer to. Say we did the following:

type IntList = [Int]

All this does is to allow us to refer to the [Int] type as IntList. They can be used interchangeably. We don’t get an IntList value constructor or anything like that. Because [Int] and IntList are only two ways to refer to the same type, it doesn’t matter which name we use in our type annotations:

*Main> type IntList = [Int]
*Main> ([1,2,3] :: IntList) ++ ([1,2,3] :: [Int])

We use type synonyms when we want to make our type signatures more descriptive by giving types names that tell us something about their purpose in the context of the functions where they’re being used. For instance, when we used an association list of type [(String,String)] to represent a phone book, we gave it the type synonym of PhoneBook so that the type signatures of our functions were easier to read.

The newtype keyword is for taking existing types and wrapping them in new types, mostly so that it’s easier to make them instances of certain type classes. When we use newtype to wrap an existing type, the type that we get is separate from the original type. If we make the following newtype:

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 CharLists, because ++ works only on lists and the CharList type isn’t a list, even though it could be said that it contains one. 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 have to derive or manually write them.

In practice, you can think of newtype declarations as data declarations that can only have 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 and with them, you can go hog wild. They can have as many constructors and fields as you wish and can be used to implement any algebraic data type by yourself. Everything from lists and Maybe-like types to trees.

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. And if you want to make something completely new, odds are good that you’re looking for the data keyword.


Posta un commento o usa questo indirizzo per il trackback.



Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.

%d blogger hanno fatto clic su Mi Piace per questo: