0 odds and ends in haskell: folding, i/o, and functors adapted from material by miran lipovaca
TRANSCRIPT
2
The foldl functionWe’ve seen a particular pattern quite often with lists: - base case on empty list - some operation with the head, plus a recursive call on the tail
This is such a common pattern that there is a higher-order function to handle it.
Inputs: a function, a initial starting value (which we’ll call the accumulator, although it can have any name) and a list to “fold up”
3
Example: implementing the sum function
sum' :: (Num a) => [a] -> a sum' xs = foldl (\acc x -> acc + x) 0 xs The binary function is applied to the accumulator and the first element (in foldl), and produces a new accumulator. Then called again with the new accumulator and the new first element of the list, until the rest of the list is empty.ghci> sum' [3,5,2,1] 11
4
In fact, we can write this function in an even shorter way, since functions can be returned as parameters:
sum' :: (Num a) => [a] -> a sum' = foldl (+) 0
The lambda function on the previous slide is really the same as (+), and we can omit xs because the function written above will just return a function that takes a list as input.
5
Another example: elemReturns True if the variable is present in the listelem' :: (Eq a) => a -> [a] -> Bool elem' y ys = foldl (\acc x > if x == y then True else acc)
False ys - Starting value and accumulator are booleans.- Start (and default) is False, which makes sense.- Check if current element is what we want. If so, done (so return True). Otherwise, accumulator is unchanged, and it continues on with the tail.
6
Other functions:
- Foldr is the same, except starts with the end of the list (and accumulator is the second input to the function).
- Scanl and scanr work just the same, but return all intermediate accumulator values in a list.
- foldl1 and foldr1 work just the same as foldl and foldr, but don’t need to provide a starting value - they assume first (or last) element of the list is the starting value.
7
File I/OSo far, we’ve worked mainly at the prompt, and done very little true input or output. This is logical in a functional language, since nothing has side effects!
However, this is a problem with I/O, since the whole point is to take input (and hence change some value) and then output something (which requires changing the state of the screen or other I/O device.
Luckily, Haskell offers work-arounds that separate the more imperative I/O.
8
A simple example: save the following file as helloword.hs
main = putStrLn "hello, world"
$ ghc --make helloworld [1 of 1] Compiling Main
( helloworld.hs, helloworld.o ) Linking helloworld ... $ ./helloworld hello, world
Now we actually compile a program:
9
What are these functions?
ghci> :t putStrLn putStrLn :: String -> IO () ghci> :t putStrLn "hello, world" putStrLn "hello, world" :: IO () So putStrLn takes a string and returns an I/O action (which has a result type of (), the empty tuple).
In Haskell, an I/O action is one with a side effect - usually either reading or printing. Usually some kind of a return value, where () is a dummy value for no return.
10
An I/O action will only be performed when you give it the name “main” and then run the program.
A more interesting example: main = do putStrLn "Hello, what's your name?” name <- getLine putStrLn ("Hey " ++ name ++ ",
you rock!") Notice the do statement - more imperative style. Each step is an I/O action, and these glue together.
11
More on getLine:
ghci> :t getLine getLine :: IO String This is the first I/O we’ve seen that doesn’t have an empty tuple type - it has a String.
Once the string is returned, we use the <- to bind the result to the specified identifire.
Notice this is the first non-functional action we’ve seen, since this function will NOT have the same value every time it is run! This is called “impure” code.
12
An invalid example:
nameTag = "Hello, my name is " ++ getLine What’s the problem? Well, ++ requires both parameters to have the same type.
What is the return type of getLine?
Another word of warning: what does the following do?
name = getLine
13
Just remember that I/O actions are only performed in a few possible places:- A main function- inside a bigger I/O block that we have composed with a do (and remember that the last action can’t be bound to a name, since that is the one that is the return type).-At the ghci prompt:
ghci> putStrLn "HEEY" HEEY
14
You can use let statements inside do blocks, to call other functions (and with no “in” part required):import Data.Char
main = do putStrLn "What's your first name?" firstName <- getLine putStrLn "What's your last name?" lastName <- getLine let bigFirstName = map toUpper firstName bigLastName = map toUpper lastName putStrLn $ "hey " ++ bigFirstName ++ " " ++
bigLastName +
+ ", how are you?" Note that <- is for I/O, and let for expressions.
15
Return in haskell: NOT like other languages.main = do line <- getLine if null line then return () else do putStrLn $ reverseWords line main reverseWords :: String -> String reverseWords = unwords map reverse . words
16
What is return?
Does NOT signal the end of execution! Return instead makes an I/O action out of a pure value.
main = do a <- return "hell" b <- return "yeah!" putStrLn $ a ++ " " ++ b
In essence, return is the opposite of <-. Instead of “unwrapping” I/O Strings, it wraps them.
17
Other I/O functions: -print (works on any type in show, but calls show first)-putStr - And as putStrLn, but no newline-putChar and getCharmain = do print True print 2 print "haha" print 3.2 print [3,4,3]
main = do c <- getChar if c /= ' ' then do putChar c main
else return ()
18
More advanced functionality is available in Control.Monad:
import Control.Monad import Data.Char main = forever $ do putStr "Give me some input: " l <- getLine putStrLn $ map toUpper l
(Will indefinitely ask for input and print it back out capitalized.)
19
FunctorsFunctors are a typeclass, just like Ord, Eq, Show, and all the others. This one is designed to hold things that can be mapped over; for example, lists are part of this typeclass.
class Functor f where fmap :: (a -> b) -> f a -> f bThis type is interesting - not like previous exmaples, like in EQ, where (==) :: (Eq a) => a -> a -> Bool. Here, f is NOT a concrete type, but a type constructor that takes one parameter.
20
Compare fmap to map:
fmap :: (a -> b) -> f a -> f b
map :: (a -> b) -> [a] -> [b]
So map is a lot like a functor! Here, map takes a function and a list of type a, and returns a list of type b. In fact, can define map in terms of fmap:
instance Functor [] where fmap = map
21
Notice what we wrote:
instance Functor [] where fmap = map We did NOT write “instance Functor [a] where…”, since f has to be a type constructor that takes one type. Here, [a] is already a concrete type, while [] is a type constructor that takes one type and can produce many types, like [Int], [String], [[Int]], etc.
22
Another example:
instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing Again, we did NOT write “instance Functor (Maybe m) where…”, since functor wants a type constructor.
Mentally replace the f’s with Maybe, so fmap acts like (a -> b) -> Maybe a -> Maybe b.
If we put (Maybe m), would have (a -> b) -> (Maybe m) a -> (Maybe m) b, which looks wrong.