There are at least four ways of defining functions in Haskell:
We’ve seen the first two. Now let’s turn to composition. You’ve almost certainly worked with composition in your math classes. What is meant by (f . g)(x)? It means g is evaluated at x, and its result is used to further evaluate f.
Composing means to chain operations together, much like pipelining at the shell. The difference between composition and pipelining or Haskell’s
$ operator is a matter of precedence. Haskell’s compose operator lets us chain functions together before we evaluate any of them. We can produce new functions by composing old ones together:
h = f . g h 5 -- these two calls will f (g 5) -- produce the same result
That means we can eliminate a lot of clutter from our code! Those gauntlets of function calls that our data must pass through can be shrunk down to a single meaningful name.
Time for some examples:
rsortwhich sorts a list in reverse:
rsort = Ord a => [a] -> [a] rsort = reverse . sort
scalebiasto map a value using a linear function:
scalebias :: Double -> Double -> Double -> Double scalebias scale bias x = scale * x + bias -- without composition scalebias' scale bias = (+ bias) . (* scale) -- with compositionNow we write
calc2fahrenlike we should have back in high school:
c2f :: Double -> Double c2f = scalebias' 1.8 32
nnegatives :: [Integer] -> Int nnegatives = length . filter (< 0)
neck :: [a] -> a neck = head . tail
onlies :: Ord a => [a] -> [a] onlies = concat . filter single . group . sort where single list = length list == 1
type Point = (Double, Double) scale :: Double -> Point -> Point scale factor (x, y) = (factor * x, factor * y) translate :: (Double, Double) -> Point -> Point translate (dx, dy) (x, y) = (x + dx, y + dy) rotate :: Double -> Point -> Point rotate degrees (x, y) = (x', y') where radians = degrees * pi / 180 x' = x * cos radians - y * sin radians y' = x * sin radians + y * cos radiansWe can make a spiral by repeatedly scaling and rotating a point. Each step will be this compound operation:
spiral degrees growthRate = rotate degrees . scale growthRateOkay, now it’s time to write a standalone Haskell program that does its own IO instead of operating in the REPL. Sadly, IO is one of those things that doesn’t fit well in a pure functional language. IO is stateful. It embeds a notion of time: first this line, then this line, and then that one… Pure function languages forbid the notion of time and state. They are about mathematical truth, which is timeless. We must leave the world of getters (expressions) and enter the world of doers (statements). Let’s start with a simple function that prints a
print2 (x, y) = printf "%.5f,%.5f\n" x yNow let’s write our first
main. It is different:
main = do let p = (1, 0) print2 p let p' = spiral 1.05 20 p print2 p' return ()We’ll talk about Haskell’s IO more another day. Right now let’s just remember these things: we must
return ()at the end, time sequences must be embedded in a
doblock, and let bind values to identifiers with
let. What if we want to execute multiple steps? We can write a recursive method.
mainwill kick it off with the initial point and number of iterations:
main = step (1, 0) 100Since there’s only one line, we don’t need
stepfunction might look this this:
step p n = do let p' = spiral 1.05 20 p print2 p' if n > 0 then step p' (n - 1) else return ()Now we can execute this script from the command-line:
runhaskell foo.hsTo make this a little more flexible, let’s accept some command-line parameters:
step :: (Double, Double) -> Double -> Double -> Int -> IO () step p rate degrees n = do let p' = spiral rate degrees p print2 p' if n > 0 then step p' rate degrees (n - 1) else return () main = do args <- getArgs let x = read (head args) :: Double let y = read (neck args) :: Double let rate = read (args !! 2) :: Double let degrees = read (args !! 3) :: Double let n = read (args !! 4) :: Int step (x, y) rate degrees n
type Rectangle = (Point, Double, Double) bl (p, _, _) = p br ((left, bottom), width, _) = (left + width, bottom) tl ((left, bottom), _, height) = (left, bottom + height) tr ((left, bottom), width, height) = (left + width, bottom + height)To rotate around an arbitrary pivot, we pass a point through this gauntlet:
rotateAround :: Point -> Double -> Point -> Point rotateAround pivot@(dx, dy) degrees = translate pivot . rotate degrees . translate (-dx, -dy)Finally, we can write a
mainto rotate a rectangle by the number of degrees provided at the command-line:
main = do args <- getArgs let degrees = read (head args) :: Double let r = (0, 0, 1, 1) print2 $ rotateAround (1, 1) degrees (bl r) print2 $ rotateAround (1, 1) degrees (br r) print2 $ rotateAround (1, 1) degrees (tr r) print2 $ rotateAround (1, 1) degrees (tl r) print2 $ rotateAround (1, 1) degrees (bl r) return ()
Here’s your TODO list:
See you next time, when we discuss lambdas and higher-order functions!
P.S. It’s Haiku Friday!
scramble = fry . whisk . crack hardboil = trash . peel . boil vandalize = freeze . throw