We don't do a lot of string processing in this course, but in Racket it works in ways that you will find familiar -- with a Racket-y prefix twist, of course. For example, two useful primitive string functions are:
> (string #\E #\u #\g #\e #\n #\e) "Eugene" > (string-ref "Eugene" 2) #\g > (string-ref (string #\E #\u #\g #\e #\n #\e) 3) #\e
Now for the exercise:
Write a function named acronym that takes one argument, a list of strings, and returns a string consisting of the first character of each string in the list.
> (acronym '("University" "Northern" "Iowa")) "UNI" > (acronym '("The" "Artist" "Formerly" "Known" "As" "Prince")) "TAFKAP"
After Session 5, you have all the tools you need to solve this problem without a loop and without recursion. You can use the ideas and functions we learned last time to do the job. How can you use map to solve part of the problem? How can you use apply to solve another part?
First, we need to get the first letter of each word. We learned last time that instead of writing a loop, we can map a function over a list. Recall that map applies a function to every item in a list and returns a list of the results.
In order for map to help us here, we need a function that takes a string and returns its first character. string-ref is close, but it requires that we tell it the position of the character to retrieve. We know that we always want the first character, so we write a one-argument helper function:
(define first-char (lambda (str) (string-ref str 0)))
When we map it over the list, we get just what we need: the first letter of each word:
> (map first-char '("University" "Northern" "Iowa")) '(#\U #\N #\I)
Now we have to put these characters together to make a string. string does that, but it takes many individual characters as arguments, not a list of characters as a single argument. That's where apply comes to the rescue:
> (apply string (map first-char '("University" "Northern" "Iowa"))) "UNI"
That's the body of the function we need, taking the list of strings as a parameter:
(define acronym (lambda (list-of-strings) (apply string (map first-char list-of-strings))))
This exercise shows how we can use our two new functions, map and apply, to combine other functions in a way that solves a problem.
Quick Exercise: Do I have to write the separate helper function first-char?
Functions are values in Racket, just like any other, so we can do without the first-char!
I can think of two possible extensions to our solution that might be helpful. First, it might be convenient if we could call acronym without "listing" its arguments, like this:
> (acronym "University" "California" "Los" "Angeles") "UCLA" > (acronym "The" "Artist" "Formerly" "Known" "As" "Prince") "TAFKAP"
We will learn one new thing about lambda today that makes this possible. The new idea will undo a simplification in our understanding of how parameters are specified.
Second, acronym would be even cooler if it omitted the little words whose initials we usually don't want to appear in our acronyms, like this:
> (acronym '("University" "of" "California" "at" "Los" "Angeles")) "UCLA"
If your Racket-fu is strong, make it so. You will need a new Racket primitive, a function similar to map, to help you!
It often easier to grow a large programs gradually from smaller parts than it is to design and write a complete solution up front. This is a good practice in most languages and most styles, but I think you'll find it especially helpful when programming in a functional style.
A language that doesn't affect the
way you think about programming
is not worth knowing.
-- Alan Perlis,
Epigrams on Programming
Last time, we learned a little about lambda, a special form that creates functions. lambda takes two arguments. The first is a list of (unevaluated) parameter names, and the second is an unevaluated expression that defines the operation to be performed. lambda expressions can be used in "raw" form or as named objects.
> ((lambda (n) (+ n 1)) 143) 144 > (define inc ; equivalent to Racket's primitive function add1 (lambda (n) (+ n 1))) > (inc 27) 28
Creating and naming functions isn't be new to you; you do it as a matter of course in other programming languages. What is new about Racket functions?
When we say that function is a first class type, we mean that we can use function in all the same ways we use numbers, strings, booleans, or any other data type. We can:
The last two capabilities on that list are different from how you are used to programming. We say that a function is higher-order if it takes a function as an argument or returns a function as its value. Being able to pass a function into or out of another function opens a door to new possibilities for us.
Last time, we learned about our first higher-order functions, apply and map. Each takes a function as an argument and uses it to compute a value. We used apply and map at the start of this session to implement our acronym function.
How about a function that returns a function as its value? *There is no new syntax to learn*. We simply write a function that returns a lambda expression as its value. Here is a function that makes special-purpose "add n" functions:
(define add (lambda (n) (lambda (m) (+ m n))))
Why might we create such a function? To use the functions it produces as part of a larger solution, often in conjunction with apply and map. If I would like to increment every number in a list, I can map Racket's add1 function:
> (map inc '(1 4 9 16 25)) '(2 5 10 17 26)What if I want to add 6 to every number in the list? I can make the incrementer I need:
> (map (add 6) '(1 4 9 16 25)) '(7 10 15 22 31)
This is a handy tool I need for creating the functions I need without writing them from scratch. But does it have more practical benefits?
This idea can be very useful. In your reading for today, you learned about a scenario in which higher-order functions of both kinds play an important role. make-validator is a "function factory": it takes two arguments (a digit-manipulating function f and a modulus m) and returns as its value a new function validates numbers. This function enables us to generate validation function for an entire family of self-verifying numbers.
Notice how, in that code, we treat a function in exactly the same way as we treat a more ordinary value, an integer.
We would have done the same thing with the modulus in a Java, Python, or Ada program. Because Racket treats functions as first-class values, we can do this with the digit function as well. Python allows us to do this, too -- though you may not have ever seen it.
In Racket, though, this is a natural part of programming that doesn't even require new syntax. We simply write a function that returns a lambda expression as its value.
The self-verifying number example is neat because it lets us see how higher-order functions matters in an application beyond the scope of this course.
The process we went through in building that code should be important to you as a programmer. We wrote a couple of functions, recognized similarities or duplication in the code, and factored the common code into its own function. This is a common way of building large programs, growing them from small examples over time.
A language that has higher-order functions gives you another tool for factoring your code.
Study this example some more, and keep your eyes open for opportunities to do the same later.
On first exposure, you might imagine that you'll never use functions such as map and apply after you finish this course, but you might be wrong... In order to do distributed computing on large data sets across clusters of computers, programmers at Google developed a technique called MapReduce. The "map" in MapReduce is essentially the same map we learned about last session. The "reduce" is a general name for the idea of combining a set of partial results into a single final answer. apply is a reducer! Our solution to the opening exercise is a simple form of MapReduce:
(apply string (map first-char list-of-strings))
It processes a list of strings to create a list of characters and then reduces that list into a single string. Next week, we will begin to learn techniques for writing other kinds of mappers and reducers. MapReduce is now available as open-source software in packages such as Hadoop, which many people use to process large data sets.
Let's now work through another problem to see how functional programmers use first-class functions to think and write code. Then we will close our discussion of functions for now by considering two more features of functions that will be helpful to us as we study languages and write interpreters: currying and variable arity.
Suppose that we are doing a medical research project involving Body Mass Index. We are processing sets of patients that have been grouped by height: everyone who is 66" tall in one group, everyone who is 67" tall in another, and so on. After retrieving a group of patients from our database, we have a list of patients, where each patient is a list that includes a unique ID, a date of birth, a height, a weight, and other data:
( ('aa (date 1960 1 2) 66 144 'DATA-aa) ('bx (date 1997 8 11) 66 124 'DATA-bx) ... ('zy (date 1979 11 8) 66 171 'DATA-zy) )
We would like to compute the BMI of each patient in our group. For Homework 5, we wrote a function to do that:
(define body-mass-index (lambda (inches pounds) (/ (pounds->kilograms pounds) (expt (inches->meters inches) 2))))
I would like to compute BMI for all the patients. How might a functional programmer approach this problem?
See the code in the session's zip file.
This style is not as foreign as it might seem! The basic process is a sequence of two steps, where we pass our data to a function and pass the result to another function. We program like this all the time, only with a sequence of statements and variables that tie the statements together.
patient_weights = (map patient->weight patients) bmis = (map (body-mass-index-at 66) patient_weights)
Those of you who use Linux and love the command line do this sort of programming all the time, too, and in a way that is almost Racket-y:
> cat session06.rkt | grep lambda | wc -l 18
In Racket, we might write
(wc '-l (grep 'lambda (cat "session06.rkt")))
The difference is more a matter of syntax than style!
Sometimes, we gain an advantage from writing functions that are capable of taking their arguments one at a time. I did that earlier in the session when I created an add-n function:
(define add (lambda (n) (lambda (m) (+ m n))))
We usually think of addition as a binary operation: a function that takes two arguments. add is a function that takes one argument and returns a function that takes the other.
Why would we ever want to do this? Suppose we are working with blocks of memory and need to index into each block to the same location, or that we need to jump ahead at fixed intervals. In these cases, we know one of the addends at the beginning of the process, but not the other. Or perhaps we are implementing a web site in which clients add items to their carts one at a time, on different web pages. We need the ability to save a partial computation until a later time, at which point we know all of the arguments.
We call functions that take their arguments one at a time curried, in honor of Haskell Curry, a mathematician who explored the idea. (An entire functional programming language, Haskell, was also named in his honor.)
The idea is so simple that we have already done it, even before we gave it a name. body-mass-index-at curries body-mass-index:
(define body-mass-index-at ; is a function ... (lambda (inches) ; that takes one argument ... ; and returns a functions ... (lambda (pounds) ; that takes the second arg (/ (pounds->kilograms pounds) (expt (inches->meters inches) 2)))))
Some programming languages, including Haskell, support currying as a primitive feature of every function. In Haskell, for instance:
f x y = x + y -- a function that adds x to y (f 1) -- a function that takes y as an arg and adds 1
Racket does not provide currying automatically, but we can write curried functions ourselves using lambda. To add two numbers, say 2 + 4, we can use add like this:
> ((add 2) 4) ;; (add 2) returns (lambda (m) 6 ;; (+ 2 m))
This same idea also allows us to name common operations. If we need to add 2 to other values frequently, we can create an add2 function that takes the second argument:
> (define add2 (curried-add 2)) > (add2 4) 6
Why might we want to do this? A curried function allows us to customize a multiple-argument function for use in a specific context. In our BMI exercise above, we needed to "hard code" the height of a cohort in order to create a function that we could map over a list of patients. Homework 3, Problem 1 gives another example. If we are always making candy in Cedar Falls, why would we want to send its elevation on every call to candy-temperature? That's a maintenance error waiting to be made, at potentially great cost.
In our study of programming languages more generally, currying teaches us something important:
A programming language does not have to support functions of more than one argument.
Functions of more than one argument are convenient, but not necessary. Any function of n > 1 arguments can be translated into a function that takes one argument and returns a fiunction of n-1 arguments as its value.
So, we can say that functions of multiple arguments are a syntactic abstraction, a language feature that is not necessary in order to be able to compute any values we might need. We will study other syntactic and data abstractions in greater detail later in the course.
Why is the idea of a syntactic abstraction important to us? A language interpreter does not need to be able to understand the abstraction. Instead it, or another program, can pre-process the abstraction out of the program before giving it to the interpreter.
This is one of the reasons why Racket and Lisp are so easy to extend: they provide macro facilities that allow the programmer to create a new syntactic abstractions simply by defining how to translate the new syntax into standard Racket. So, a programmer can add arbitrarily complex expressions to the language without modifying the interpreter or compiler!
Will we find practical uses for curried functions in this course? You bet! We'll use the idea occasionally this semester as we write code to process programs. (I hope you recognize the idea when it comes along. I will point it out if you don't.)
Arity refers to the number of arguments that a function accepts. For example, sqrt is usually a unary operation: it accepts exactly one argument. For generality's sake, you will sometimes see this written as 1-ary, and we can say that sqrt has an arity of 1. In many languages, addition and subtraction are binary operations and have an arity of 2.
All the functions that we have written thus far have taken a fixed number of arguments. But we have used several standard Racket functions that can take any number of arguments, such as +, list, and string. We say that such functions are or have variable arity. There are very few features of Racket's primitives that we cannot mimic directly when writing programs in Racket. So you should not be surprised to learn that we can write variable arity functions in Racket, too!
To create a variable arity function, we will make a small change to the syntax of lambda. We do not know how many arguments will be passed, so we cannot enumerate them and give each its own parameter name. Instead, we will give a single name, without (), to name a list that contains all of the arguments that are passed to the function.
Last session, we needed an average function to compute GPAs for a list of students. I gave that function to you in the session's code file:
(define average (lambda numbers (/ (apply + numbers) (length numbers))))
Notice: There are no parentheses around the parameter numbers. We don't know how many arguments the caller will send, so we can't name them individually. Instead, we tell lambda to take all the arguments, however many, and put them in a list named numbers. The body of the function can then act on the list, whether it contains 2, 100, or even 0 items.
In this case, we can write the body of average function using existing Racket functions, including the variable-arity + function and the higher-order apply function.
If we aren't so lucky, we will have to write a recursive function so that it can process the list of values one by one. Next week, we will begin to discuss techniques for processing lists and other data types recursively. After you learn those techniques, implementing a variable-arity function like + will seem straightforward.
Quick Tidbit: Did you know that you can create variable arity functions in Python and C, too? Take a look at a simple Python example and a simple C++ example, both of which accept any number of arguments.