Functional Programming - SLU Mathematics and Computer Science

Download Report

Transcript Functional Programming - SLU Mathematics and Computer Science

Odds and Ends
in Haskell:
Folding, I/O, and Functors
Adapted from material by Miran Lipovaca
0
The foldl function
We’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”
1
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
2
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.
3
Another example: elem
Returns True if the variable is present in the list
elem' :: (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.
4
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.
5
File I/O
So 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.
6
A simple example: save the following file as
helloword.hs
main = putStrLn "hello, world"
Now we actually compile a program:
$ ghc --make helloworld
[1 of 1] Compiling Main
( helloworld.hs, helloworld.o )
Linking helloworld ...
$ ./helloworld
hello, world
7
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.
8
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.
9
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.
10
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
11
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
12
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.
13
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
14
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.
15
Other I/O functions:
-print (works on any type in show, but calls show
first)
-putStr - And as putStrLn, but no newline
-putChar and getChar
main = 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 ()
16
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.)
17
Functors
Functors 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 b
This 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.
18
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
19
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.
20
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.
21
Using it:
ghci> fmap (++ " HEY GUYS IM INSIDE THE
JUST") (Just "Something serious.")
Just "Something serious. HEY GUYS IM INSIDE TH
E JUST"
ghci> fmap (++ " HEY GUYS IM INSIDE THE
JUST") Nothing
Nothing
ghci> fmap (*2) (Just 200)
Just 400
ghci> fmap (*2) Nothing
Nothing
22