teaching machines

CS 330 Lecture 23 – Ways of Making Functions: Composition

Dear students,

There are at least four ways of defining functions in Haskell:

  • by defining a named function
  • by partially applying parameters to get a new function expecting only the remaining parameters
  • by composing a gauntlet of functions together
  • by defining unnamed lambdas

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:

  1. Let’s write rsort which sorts a list in reverse:
    rsort = Ord a => [a] -> [a]
    rsort = reverse . sort
  2. Let’s write scalebias to 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 composition
    Now we write calc2fahren like we should have back in high school:
    c2f :: Double -> Double
    c2f = scalebias' 1.8 32
  3. Let’s write a function to compute the number of negatives in a list:
    nnegatives :: [Integer] -> Int
    nnegatives = length . filter (< 0)
  4. Let’s write a function that gives us the second element of a list, the neck:
    neck :: [a] -> a
    neck = head . tail
  5. Let’s write a function that gives us a list of the unique items within another list:
    onlies :: Ord a => [a] -> [a]
    onlies = concat . filter single . group . sort
      where single list = length list == 1
  6. Let’s write a function to create a spiral. As a first stop, let’s write some utility type synonyms and the (big three) functions to transform a 2D point:
    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 radians
    We can make a spiral by repeatedly scaling and rotating a point. Each step will be this compound operation:
    spiral degrees growthRate = rotate degrees . scale growthRate
    Okay, 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 Point:
    print2 (x, y) = printf "%.5f,%.5f\n" x y
    Now 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 do block, and let bind values to identifiers with let. What if we want to execute multiple steps? We can write a recursive method. main will kick it off with the initial point and number of iterations:
    main = step (1, 0) 100
    Since there’s only one line, we don’t need do. The step function 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.hs
    To 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
  7. Let’s write a function to transform a rectangle around an arbitrary pivot point. Let’s represent with rectangle with four numbers: the x- and y-coordinates of the bottom-left corner, a width, and a height. Let’s create some helper functions to determine the coordinates of the points:
    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 main to 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!

Sincerely,

P.S. It’s Haiku Friday!

scramble = fry . whisk . crack
hardboil = trash . peel . boil
vandalize = freeze . throw

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *