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 '("National" "Basketball" "Association")) "NBA" > (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.
Think about the data you are given and steps on the way to the solution... You are given a list of strings. What step can you take that would get you closer to a solution?
How can map help us here?
We are given a list of strings. What we really want is a list containing the first character of each of those strings. So, first of all, we need a function that returns the first character of a string.
Last time we learned that, instead of writing a loop, we can map a function over a list. map applies a function to every item in a list and returns a list of the results.
With map and a "first char" function, we can produce a list containing the first character of each of the strings.
Do we have 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. In this case, we always want the first character, so we can write a one-argument helper function:
(define first-char (lambda (str) (string-ref str 0)))
When we first-char it over the list, we get just what we need:
> (map first-char '("National" "Basketball" "Association")) '(#\N #\B #\A)
Now we have to put these characters together to make a string. string does that. However, it takes any number of 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 '("National" "Basketball" "Association")) "NBA"
That's the body of the function we need, with the list of strings to process taken as an argument:
(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. It also illustrates how functional programmers think about problems and what programs look like when we get done.
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 a function named 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 "National" "Basketball" "Association") "NBA" > (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 expand 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" "Northern" "Iowa")) "UNI" > (acronym '("University" "of" "California" "at" "Los" "Angeles")) "UCLA"
If your Racket-fu is strong, make it so [optional]. You will need a new Racket primitive -- a function similar to map -- to help you! We will see that new function in class next time.
Notice, too, the style in which we wrote acronym. It is 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 about lambda, the 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 ; that lambda is equivalent to Racket's primitive function add1 > (add1 27) 28
Creating and naming functions isn't 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.
But we left some unfinished business...
What about a function that returns a function as its value? There is no new syntax to learn.
We can write a function as a value using lambda. If the body of a function is a lambda expression, then that is the value it returns.
Why might we want to write such a function? In order to create functions to use as part of a larger solution.
For example, if I need to increment every number in a list by one, I can map Racket's add1 function:
> (map add1 '(1 4 9 16 25)) '(2 5 10 17 26)What if I need to add some other value to every number in the list, say 6?
> (map add6 '(1 4 9 16 25)) add6: undefined; cannot reference an identifier before its definitionAlas, there is no add6 function in Racket.
We could define add6 as:
(define add6 (lambda (x) (+ x 6)))That works, but is limiting. What if later I need to add some other value to every number in the list, say 10 or 12?
Instead, I can write a function that makes special-purpose "add n" functions:
(define add ; add is function (lambda (n) ; that takes one argument ;---------- and returns (lambda (m) ; a one-argument function (+ m n)) ; that adds the two numbers ;---------- ))
Now I can add 6 to every number in the list with:
> (map (add 6) '(1 4 9 16 25)) '(7 10 15 22 31)or 10:
> (map (add 10) '(1 4 9 16 25)) '(11 14 19 26 35)or 12:
> (map (add 12) '(1 4 9 16 25)) '(13 16 21 28 37)
If we really want a function named add6, we can now create it with a one-liner:
(define add6 (add 6))
A language that has higher-order functions gives you a new tool for customizing your code. Languages that do not support them limit your ability to write programs.
This is a handy tool I need for creating the functions I need without writing them from scratch. But as you should know by now, it has practical benefits in other settings.
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 integer in a Java, Python, or C program. Because Racket treats functions as first-class values, we can do this with the digit function as well. Python and Java allow us to do this, too, though you may not have seen it yet.
We can then use the common framework of the two functions to create a validation function from a modulus and a digit function.
This is possible in some other languages, though usually with some syntactic gymastics. 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. Languages that do not support them limit your ability to write programs.
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.
A few years ago, I ran across a programming challenge called Advent of Code. It poses a problem every day from December 1 through Christmas and asks programmers to solve it with a program. In December 2019, the challenge for Day 1 boiled down to this:
You are given a file listing the masses of number of modules, one per line. Compute the total amount of fuel needed to send all of the modules into space. The fuel required to launch one module is based on its mass: divide the mass of the module by three, round down, and subtract two.
We haven't read data from files yet, but Racket provides several useful functions. One is the primitive function file->lines, which will seem familiar to some Python programmers:
> (file->lines "modules.txt") '("12" "14" "1969" "100756")
We would like to write a function that takes a filename as an argument and returns the total fuel needed to send all of the modules into space:
> (total-fuel "modules.txt") 34241
How might a functional programmer approach this problem? In much the same way as any other programmer:
The big difference is that functional programmers are always asking themselves What functions can help me here?, keeping in mind higher-order functions such as apply and map.
At each step, we use a function to help us, writing any function we don't already have available to us...
This function isn't really a function... It depends on an external resource: the input file! How can we make it more general?
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.
strings = (file->lines filename) numbers = (map string->number strings) fuels = (map module->fuel numbers) total = (apply + fuels)
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!
Note: We did not cover this in class separately. Please read it carefully. It contains two important ideas that we will return to many times this semester!
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. add curries +:
(define add ; is a function ... (lambda (n) ; that takes one argument ... (lambda (m) ; and returns a function ... (+ m n)))) ; that takes the second arg
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 6 + 4, we can use add like this:
> ((add 6) 4) ;; (add 6) returns (lambda (m) 10 ;; (+ 6 m))
This same idea also allows us to name common operations. If we need to add 6 to other values frequently, we can create an add6 function that takes the second argument:
> (define add6 (add 6)) > (add6 4) 10
Why might we want to do this? A curried function allows us to customize a multiple-argument function for use in a specific context. Our add-n function does that. 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 function 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.
This is one of the reasons why Racket and Lisp are so easy to extend: they provide macro facilities that allow programmers to create a new syntactic abstraction 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!
We will return to these two big ideas in Unit 3 of the course, Syntactic Abstraction.
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. Three sessions from now, 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.