|
|
Functional programming with Haskell By Chris Dutton ModulesSo, we've seen input, output, strings, functions, conditionals, lists, tuples, and some very handy functions like map and mapM_. What's next? Well, looking at the above program, most of it handles greeting people, and then one lonely function is the main function where everything happens. What if I want to use the functions related to greeting in another program but I want a different main? Well, then I need to put all of those functions into their own module. Let's call the new module Greeting. Naturally, it'll be located in the file Greeting.hs. module Greeting where greet :: (String,Int) -> IO () greet name = putStrLn $ greeting name gatherNames :: IO [String] gatherNames = do putStr "You are? " name <- getLine if name == "quit" then return [] else do otherNames <- gatherNames return $ name : otherNames printAll :: [String] -> IO () printAll = mapM_ putStrLn greetings :: [(String,Int)] -> [String] greetings = map greeting getAge :: IO Int getAge = do putStr "And you're how old? " input <- getLine let parsed = reads input if parsed == [] then do putStrLn "I'm sorry, but could you repeat that?" getAge else return $ fst $ parsed !! 0 gatherInfo :: IO [(String, Int)] gatherInfo = do putStr "You are? " name <- getLine if name == "quit" then return [] else do age <- getAge let info = (name,age) otherInfo <- gatherInfo return $ info : otherInfo greeting :: (String,Int) -> String greeting (name,age) | age < 12 = "Do your parents know where you are, " ++ name ++ "?" | age > 80 = "Do your children know where you are, " ++ name ++ "?" | name == "Haskell" = "Hey, whadda ya know? This is a Haskell program!" | name == "Matz" = "You make a good language." | otherwise = "Hello, " ++ name ++ "!" No our Main module simply looks like: module Main where import Greeting main :: IO () main = do info <- gatherInfo printAll $ greetings info Of course, if you want it to be explicit where the functions you're using come from, you can prepend the name of the module. module Main where import Greeting main :: IO () main = do info <- Greeting.gatherInfo Greeting.printAll $ Greeting.greetings info This can be encorced by using the "qualified" import modifier. module Main where import qualified Greeting main :: IO () main = do info <- Greeting.gatherInfo Greeting.printAll $ Greeting.greetings info And with either we can limit the functions we import. module Main where import Greeting (gatherInfo, printAll, greetings) main :: IO () main = do info <- gatherInfo printAll $ greetings info Introducing our own data typesOf course, at this point, we've used a new data type in the form of a tuple. However, we're counting on being able to recognize that a tuple consisting of a string and an integer is a person. Haskell gives us the power to be more expressive, by introducing new data types. In this case it's really quite simple. data PersonInfo = Person String Int
This introduces a new type called PersonInfo with a single constructor which takes a string and an int. So, let's start by simply redefining the greeting function to take advantage of this new data type. greeting :: PersonInfo -> String greeting (Person name age) | age < 12 = "Do your parents know where you are, " ++ name ++ "?" | age > 80 = "Do your children know where you are, " ++ name ++ "?" | name == "Haskell" = "Hey, whadda ya know? This is a Haskell program!" | name == "Matz" = "You make a good language." | otherwise = "Hello, " ++ name ++ "!" And, modifying the rest of our code to use this new data type, we end up with: module Greeting where data PersonInfo = Person String Int greet :: PersonInfo -> IO () greet p = putStrLn $ greeting p printAll :: [String] -> IO () printAll = mapM_ putStrLn greetings :: [PersonInfo] -> [String] greetings = map greeting getAge :: IO Int getAge = do putStr "And you're how old? " input <- getLine let parsed = reads input if parsed == [] then do putStrLn "I'm sorry, but could you repeat that?" getAge else return $ fst $ parsed !! 0 gatherInfo :: IO [PersonInfo] gatherInfo = do putStr "You are? " name <- getLine if name == "quit" then return [] else do age <- getAge let info = Person name age otherInfo <- gatherInfo return $ info : otherInfo greeting :: PersonInfo -> String greeting (Person name age) | age < 12 = "Do your parents know where you are, " ++ name ++ "?" | age > 80 = "Do your children know where you are, " ++ name ++ "?" | name == "Haskell" = "Hey, whadda ya know? This is a Haskell program!" | name == "Matz" = "You make a good language." | otherwise = "Hello, " ++ name ++ "!" To arrive at this, I made very few changes to the code I had previously. This should demonstrate quite nicely the expressive power of Haskell. Greeting other thingsNow, we've defined a set of functions useful for greeting a person. However, people are not the only things we may want to greet. Consider, for instance, the case where we want to greet a dog. For the purposes of our program a dog will be described by its name, build and color. Based on these things we'll formulate a greeting. First thing's first, though. Let's define the data types we'll need. data Build = Skinny | Medium | Fat data Color = White | Black | Gray | Red | Brown data DogInfo = Dog String Build Color In the build and Color data types, we have a set of constructors which take no arguments. Any one of these constructors creates a value of type Build or Color. This is somewhat analogous to the idea of enumerated types or "enums" in other languages. The Dog constructor then uses these data types. Let's talk about classesFor the purposes of this document, please foget what you know about classes in object-oriented languages like C++, Java, C#, Python, Ruby, Eiffel, etc. When we talk about classes in Haskell, we're talking about classifying data types, according to what functions we can use on them. The advantage of classifying data types in this way is that we don't need to be as specific when declaring a function. Consider my current declaration of the greeting function. greeting :: PersonInfo -> String This requires that the argument be of type PersonInfo. Given this, we can't simply define greeting to take an argument of type DogInfo. What we want instead is to classify data types based on whether or not we can greet them. So, let's create a class that does just that. class Greetable a where greeting :: a -> String This is fairly straightforward. Here "a" represents any type in the Greetable class. Let's add a few more functions related to the first. class Greetable a where greeting :: a -> String greetings :: [a] -> [String] greet :: a -> IO () Now, the interesting thing about classes in Haskell is that not only can we specify the types of these functions, but if we have related functions which simply depend upon another, we can provide a default definition. Now, when we later define greeting, we get the other two automatically. class Greetable a where -- declarations greeting :: a -> String greetings :: [a] -> [String] greet :: a -> IO () -- default definitions greetings = map greeting greet name = putStrLn $ greeting name One thing to notice is that I've used comments for the first time. Anything following -- on a line is a comment in Haskell. InstancesA data type is admitted to a class by means of an instance declaration. This is where we define the functions required by the class. To admit the PersonInfo data type into the Greetable class. instance Greetable PersonInfo where greeting (Person name age) | age < 12 = "Do your parents know where you are, " ++ name ++ "?" | age > 80 = "Do your children know where you are, " ++ name ++ "?" | name == "Haskell" = "Hey, whadda ya know? This is a Haskell program!" | name == "Matz" = "You make a good language." | otherwise = "Hello, " ++ name ++ "!" This looks pretty similar to our previous definition of greeting, so there's not a lot new to learn here. So, now our existing program looks like: module Greeting where data PersonInfo = Person String Int deriving (Show, Eq) class Greetable a where -- declarations greeting :: a -> String greetings :: [a] -> [String] greet :: a -> IO () -- default definitions greetings = map greeting greet x = putStrLn $ greeting x instance Greetable PersonInfo where greeting (Person name age) | age < 12 = "Do your parents know where you are, " ++ name ++ "?" | age > 80 = "Do your children know where you are, " ++ name ++ "?" | name == "Haskell" = "Hey, whadda ya know? This is a Haskell program!" | name == "Matz" = "You make a good language." | otherwise = "Hello, " ++ name ++ "!" gatherNames :: IO [String] gatherNames = do putStr "You are? " name <- getLine if name == "quit" then return [] else do otherNames <- gatherNames return $ name : otherNames printAll :: [String] -> IO () printAll = mapM_ putStrLn getAge :: IO Int getAge = do putStr "And you're how old? " input <- getLine let parsed = reads input if parsed == [] then do putStrLn "I'm sorry, but could you repeat that?" getAge else return $ fst $ parsed !! 0 gatherInfo :: IO [PersonInfo] gatherInfo = do putStr "You are? " name <- getLine if name == "quit" then return [] else do age <- getAge let info = Person name age otherInfo <- gatherInfo return $ info : otherInfo Of course, none of these changes influences our Main module in the slightest. Sidetracking: the Show classSo we've seen one class of our own design. There are other classes already in Haskell 98. One useful class is the Show class, which requires that the function "show" be defined. That class would look like: class Show a where show :: a -> String This provides a convenient means to get a string representation of something. For instance, to print an int, we'd use something like the following, since putStrLn can only deal with strings. putStrLn $ show 42 Or to add an integer to a string: "Foo " ++ show 42
Let's consider a more pertinent situation. Previously, we had defined a data type Color. data Color = White | Black | Gray | Red | Brown Now, at some point we might wish to actually get a string containing "White" or "Red". We can achieve this by making Color an instance of Show. instance Show Color where show White = "White" show Black = "Black" show Gray = "Gray" show Red = "Red" show Brown = "Brown" It seems like there should be an easier way to achieve such a simple transformation. Well, for such a basic class, we can make Color a member of the Show class without an instance declaration. data Color = White | Black | Gray | Red | Brown deriving (Show) Of course, in the course of this program we might want "red" rather than "Red". Fortunately, we can simply use map in this case. A string in Haskell, you see, is simply a list of characters. First, though, we have to import the toLower function from the Char module. import qualified Char (toLower) Then we can easily apply the map. map Char.toLower $ show Red Back on topic: greeting a dogSo, we have a Greetable class that requires members of the class define a greeting function and gives us a couple of free functions if we do. class Greetable a where -- declarations greeting :: a -> String greetings :: [a] -> [String] greet :: a -> IO () -- default definitions greetings = map greeting greet name = putStrLn $ greeting name And we have a few new data types. data Build = Skinny | Medium | Fat data Color = White | Black | Gray | Red | Brown deriving (Show) data DogInfo = Dog String Build Color What we need is to make DogInfo an instance of Greetable. instance Greetable DogInfo where greeting (Dog _ Skinny _ ) = "There's hardly anything to that dog." greeting (Dog _ Fat _ ) = "What a fat dog!" greeting (Dog name _ color) = "What a healthy " ++ (map Char.toLower $ show color) ++ " dog. Hello, " ++ name ++ "!" This example shows one important new feature of Haskell. Namely, what's the deal with all of the underscores? Well, in Haskell, when matching a pattern, you have to include all of the arguments. Of course, sometimes you just don't care what the argument is. When we greet a skinny or fat dog, we're rude and don't care what the dog's name or color is. We're just going to insult it anyway. When we don't care about an argument, we use an underscore. This signifies that yes there's an argument there, but we don't care enough to give it a name. In the end, I have the following program. module Greeting where import qualified Char (toLower) data PersonInfo = Person String Int class Greetable a where -- declarations greeting :: a -> String greetings :: [a] -> [String] greet :: a -> IO () -- default definitions greetings = map greeting greet x = putStrLn $ greeting x instance Greetable PersonInfo where greeting (Person name age) | age < 12 = "Do your parents know where you are, " ++ name ++ "?" | age > 80 = "Do your children know where you are, " ++ name ++ "?" | name == "Haskell" = "Hey, whadda ya know? This is a Haskell program!" | name == "Matz" = "You make a good language." | otherwise = "Hello, " ++ name ++ "!" printAll :: [String] -> IO () printAll = mapM_ putStrLn getAge :: IO Int getAge = do putStr "And you're how old? " input <- getLine let parsed = reads input if parsed == [] then do putStrLn "I'm sorry, but could you repeat that?" getAge else return $ fst $ parsed !! 0 gatherInfo :: IO [PersonInfo] gatherInfo = do putStr "You are? " name <- getLine if name == "quit" then return [] else do age <- getAge let info = Person name age otherInfo <- gatherInfo return $ info : otherInfo data Build = Skinny | Medium | Fat data Color = White | Black | Gray | Red | Brown deriving (Show) data DogInfo = Dog String Build Color instance Greetable DogInfo where greeting (Dog _ Skinny _ ) = "There's hardly anything to that dog." greeting (Dog _ Fat _ ) = "What a fat dog!" greeting (Dog name _ color) = "What a healthy " ++ (map Char.toLower $ show color) ++ " dog. Hello, " ++ name ++ "!" The end resultAt this point, I can greet either a person or a dog using the same function, even though the two share nothing in common except the greeting function. Of course, what more do they really need to share? do greet $ Person "John" 19 greet $ Dog "Fido" Medium Brown And we see on the screen: Hello, John! What a healthy brown dog. Hello, Fido!
Next Page
|
![]() |
This site best viewed in a W3C standard browser at 800*600 or higher Site design by Red Squirrel | Contact © Copyright 2012 Ryan Auclair/IceTeks, All rights reserved |