`

haskell - Functors, Applicative Functors and Monoids - newtype keyword

阅读更多

so far, we have learnt the data keyword, and we can also learned how to create types synonyms with the type keywrod.   (do not confuse that with the class keyword)

 

we'll be taking a look at how to make new types out of existing data types by using the newtype keyword and why we'd want to do that in the first place.

 

remember that we have said that we have examined the zipList, and let''s see how we will implements the zipList methods. 

data ZipList a = ZipList [a]  

 but we will like to name a field called getZipList, here is new 

data ZipList a = ZipList { getZipList :: [a] }  

 

This looks fine and would actually work pretty well. We had two ways of making an existing type an instance of a type class, so we used the data keyword to just wrap that type into another type and made the other type an instance in the second way.

The newtype keyword in Haskell is made exactly for these cases when we want to just take one type and wrap it in something to present it as another type. In the actual libraries, ZipList a is defined like this:

 

and the zipList with the ZipList definition is like this:

newtype ZipList a = ZipList { getZipList :: [a] }  

 well, not too much difference except that the keyword has been changed from "data" to "newtype"? huh??

 

Instead of the data keyword, the newtype keyword is used. Now why is that? Well for one, newtype is faster. If you use the datakeyword to wrap a type, there's some overhead to all that wrapping and unwrapping when your program is running. But if you use newtype, Haskell knows that you're just using it to wrap an existing type into a new type (hence the name), because you want it to be the same internally but have a different type. With that in mind, Haskell can get rid of the wrapping and unwrapping once it resolves which value is of what type.

 

well, why not to use newtype all the time?

 

for 1, newtype ,is restricted to only one constructor with one field. 

for 2, you cannot make data types that have several value constructors and each has zero or more fields , such as enum types as 

data Profession = Fighter | Archer | Accountant  

 

like data types, the newtype types can also do deriving. 

 

suppose that we make a newtype "CharList"

newtype CharList = CharList { getCharList :: [Char] } deriving (Eq, Show)  

 

so, the constructor type is something like this:

 

CharList :: [Char] -> CharList  

 and the field has the type, conversely as this 

getCharList :: CharList -> [Char]  

 

Now, suppose that we will make a newtype, which is called Pair, which is like a tuple, but because we cannot just use Maybe on a tuple and fmap on the first parameter, we get around this by definig the afore-mentioned pair. 

 

newtype Pair b a = Pair { getPair :: (a,b) }  

 

the following will show hte full code that does the type definition, instance implementation, and others. 

-- file
--    newtype_pair.hs
-- description:
--   with newtype define a pair class which allows us to fmap on the first 
--   parameter of a pair
import Control.Applicative

newtype Pair a b = Pair { getPair :: (a, b) }

instance Functor (Pair  c) where
   fmap f (Pair (x, y)) = Pair (f x, y)


-- run it you can see the following 
-- ghci> getPair $ fmap (*100) (Pair (2,3))  
-- (200,3)  
-- ghci> getPair $ fmap reverse (Pair ("london calling", 3))  
-- ("gnillac nodnol",3)  

 

 

We mentioned there is "for 1" on why to choose "newtype" over "data" when it is allowed. the second reason of "newtype" being advantage to "data" is that it is Lazy.

 

Lazy 

Haskell being lazy, you heard it, right? so, what does lazy mean here? let's first examine the "undefined" value. 

 

if you run the following on the ghci command line,  you will get an exception.

ghci> undefined  
*** Exception: Prelude.undefined  

 

But, you won't get any exception when you are running this. 

 

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

 

because the later won't be examine (and it is not necessary to examine ).

 

this is one of the live example of the lazyness, but we can go further and see lazyness in other situation. 

 

suppose that we can define some bool like type, and it is now with the data definition. 

 

data CoolBool = CoolBool { getCoolBool :: Bool }  

 

and we define a function called heloMe, which disregard the value contained in CoolBool, and print Hello as always.

 

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

 

and now will throw a curveball (some unexpected situation) to it and see how it react. here it is. 

 

ghci> helloMe undefined  
"*** Exception: Prelude.undefined  

 

ah, it throws an exception , that is because data declared types can possibly have more than one constructor, and it has to go through evaluating the values before it can tell which will be applied, and the trivial evaluation that gives it away.

 

Let's now avert to the use the newtype declaration.

 

newtype CoolBool = CoolBool { getCoolBool :: Bool }

 and we run it again with the following code. 

ghci> helloMe undefined  
"hello"  

 why ????

 

the reason is that  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!

 

 

so the rule of thumb. 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.

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics