In this exercise, we will look at some unusual ways Lisp-family languages handle functions, and the power this provides.
To begin, make a new 05 subdirectory in your 214/labs/ directory, and then make a src subdirectory inside extraCredit.
To start this exercise, open two terminal windows.
In one of the windows, open a text editor that supports parentheses matching so that you can conveniently enter and edit multiline Clojure functions. We'll call this window your text editor window.
Then type Ctrl-d twice, once to quit clojure, and again to quit script. This will return you back to the command-line in your terminal window. There, enter:
cat 0.scriptand you should see a recording of what you just did. For each of the sections below, you should repeat this procedure using a different script-file name (e.g., 1.script, 2.script, etc.) so that at the end, this collection of script files will form a record of your activities during this exercise.
Using the previous procedure, start script and clojure to make a recording called 1.script.
Thus far, we have been creating named functions in Clojure using defn. However, defn is really just a means of assigning a name to a function. We can also create unnamed or anonymous functions.
In original LISP, the function to do this was named lambda, and so these were called lambda functions. That terminology is still commonly used today for functions that have no name. In this exercise, we will use "lambda function" and "anonymous function" interchangeably.
The Clojure syntax for an anonymous function is as follows:
(fn [ParameterList] FunctionBody )where fn is a reserved word of the language, ParameterList is sequence of parameter names (which may be empty), and FunctionBody is one or more syntactically correct expressions.
Using the above form, we might define a simple anonymous function that takes a single value x as its argument, and returns twice the value of x:
(fn [x] (+ x x) )
Enter this expression into your REPL window, and note what it returns as a comment in the buffer. This return value indicates that the expression returns a function. It is important to note that the fn() function returns a function object. That is, this is a syntactic operation that create a function that has no name.
Since a lambda expression returns a function, we can also then call the function that a lambda expression creates and pass arguments to it. Enter this in your REPL window:
((fn [x] (+ x x)) 21)This lambda expression creates and returns a function object, which is then passed the value of 21 as its argument. Note that the extra parentheses -- before the (fn and after the 21 are necessary to call the anonymous function.
Likewise, the lambda expression:
((fn [x y] (+ (* x x) (* y y))) 3 4)creates a function that returns the sum of the squares of its two parameters, and then calls that function, passing 3 and 4 to it as the arguments. In your REPL window, verify that this works correctly.
Enter Ctrl-d twice to quit clojure and script, so that this much is saved in your file 1.script. Use cat to verify its contents before proceeding.
In your text-editor window, build an expression that returns a function that finds the maximum of the absolute values of three numbers. Hint: absolute value Math/abs() and maximum max() are built in functions in Clojure; Math/abs() takes a single argument; max() takes an arbitrary number of arguments.
When you think you have it correctly built, use your REPL window to test your anonymous function. Verify that it works correctly for arbitrary combinations of positive and negative arguments.
When it works correctly, quit clojure and use the script+clojure procedure to create a script-file 1a.script, in which you show that your function works correctly.
For each of the following expressions, use the function in an expression that calls the function and passes it a valid argument list. The argument list supplied should cause the function to be evaluated with no errors.
(fn [x y z] (+ x y z)) (fn [x] (nil? x)) (fn [] 17.2)When you have done so, use our script+clojure procedure to create a script-file 1b.script, in which you show each of your expressions working correctly. Use cat to verify that this file is correct before proceeding.
In your REPL window, use our script+clojure procedure to create a script-file 2.script.
Functions are first order objects in LISP-family languages, which means that we can pass functions as arguments to other functions! In order to pass a function, we need a way to reference it. There are several different approaches, including:
Using the def() Function. To illustrate the first approach, if we enter:
(def triple (fn [x] (* x 3)))fn() will return an anonymous function; def() will then bind that anonymous function to the name triple so that it is no longer anonymous.
Once you have done this, what does the following expression produce?
(triple 4)It may be helpful to think of the defn() function as a contraction of def() and fn(). That is, instead of defining triple() as we did above, we could have done so more simply using defn():
(defn triple [x] (* x 3))The def() function is more general than defn(). Where defn() is only for defining functions, def() can be used to bind any identifer to any expression.
Short-Cut Notation. Clojure also supports a short-cut notation for creating anonymous functions. To illustrate it, try entering this:
(#(* % 3) 4)In this expression, the strange-looking notation:
#(* % 3)is a short-cut for:
(fn [x] (* x 3))In short-cut notation, % refers to the function's first argument. When a function has more than one argument, %1 can be used to access the first one, %2 can be used to access the second one, and so on. Thus, instead of writing:
(fn [x y] (+ (* x x) (* y y)))we could have written:
#(+ (* %1 %1) (* %2 %2))Try this, and call it with the arguments 3 and 4 to verify that it works.
Combining def() and Short-Cut. These two approaches can be combined; the expression:
(def sum2Squares #(+ (* %1 %1) (* %2 %2)))uses the short-cut notation to build and return an anonymous function, and then binds it to the name sumSquares. Try this. What does the following expression produce?
(sum2Squares 3 4)
Quit so that this much is saved. Use cat to verify that 2.script contains a recording of what you just did.
Using your text-editor and REPL window (as necessary) to define an anonymous function that takes single argument, and returns the square of the square of that argument. Then use a def expression to bind that function to an identifier named squareSquare.
Using our script+clojure procedure, create a script file 2a.script containing your definition of squareSquare and show that it works correctly for 2, -2, 3, and -3. Use cat to verify that 2a.script is correct before continuing.
You are probably asking, "What is the point of defining an anonymous function if we just bind it to an identifier?" Good question!
The real power of this approach is that we can:
In this part of the exercise,
we'll use the REPL window to explore these a bit,
so use our script+clojure procedure
to create a script file 3.script
in which you record what you do.
The map() Function. One Clojure function that takes another function as its argument is the map() function. Try these; note what result each expression produces and see if you can figure out what each is doing:
(map inc [1 2 3 4]) (map - [1 2 3 4]) (map - [1 2 3 4] [1 2 3 4]) (map even? [1 2 3 4]) (map str ["a" "b" "c"] ["A" "B" "C"]) (map count [[11] [11 22] [11 22 33]])As you can see, the map() function's first argument is another function! If that function-argument is a unary function, then the following argument needs to be a sequence (i.e., a vector or a list), in which casemap() builds and returns a list containing the result of applying that function to each value in the sequence.
If that function-argument is a binary function and the subsequent arguments are both sequences, then map() builds and returns a list containing the result of applying that function to the corresponding values in the two sequences. Try these:
(map + [1 2 3 4] [1 2 3 4]) (map * [1 2 3 4] [1 2 3 4])
To see how anonymous functions can be useful, try entering these:
(Math/abs -3) (map Math/abs [-1 2 -3 4 -5])The first one should work fine, but the second one produces a nasty error message. The problem is that Math/abs() is Java's abs() function, which is a static method in Java's Math library, and static functions cannot be passed as arguments.
Anonymous functions to the rescue! Instead of using Math/abs(), we might try something like this:
(map (fn [x] (if (< x 0) (- x) x)) [-1 2 -3 4 -5])Give it a try -- does it work?
Here, we solve our previous problem by replacing Math/abs with our own anonymous "absolute value" function (shown in red). If we pass our anonymous function to map(), it then applies our function to the same vector of values as before. Since our anonymous function is not static, it does not suffer from the same issue as our previous attempt. Verify that this works correctly before proceeding.
We can also use anonymous / lambda functions to define other functions. For example, try this:
(defn subtract-n [n sequence] (map (fn [x] (- x n)) sequence) )Given the above definition, what does the following produce?
(subtract-n 2 [5 6 7 8])
The filter() Function. Another function that takes a function as its argument is the filter() function. What do the following produce?
(filter odd? [1 2 3 4]) (filter even? [1 2 3 4]) (filter neg? [1 2 3 4]) (filter neg? [-1 1 -2 2 -3 3]) (filter pos? [-1 1 -2 2 -3 3])The filter() function's first argument must be a predicate -- a function that returns true or false -- and its second argument is a collection, usually a list or vector. As these examples indicate, filter() applies the predicate to each item in the collection and returns the corresponding collection of items for which the predicate returns true.
The predicate passed to filter() can also be an anonymous function; it just has to return true or false. To illustrate, suppose we wanted to retrieve only the positive and even values from a collection of numbers? Try this:
(filter (fn [x] (and (pos? x) (even? x))) [-1 1 -2 2 -3 3 -4 4])This expression builds an anonymous predicate (shown in red) that returns true if and only its parameter x is both positive and even, applies that function to each number in the collection, and returns the collection of numbers for which the predicate is true.
Using this as a model, construct an expression that will retrieve the numbers from a collection that are negative or odd. For example, given the collection:
[-1 1 -2 2 -3 3 -4 4]your function should return [-1 1 -2 -3 3 -4].
Quit clojure and script, and use cat to verify that this part of the exercise has been correctly saved in 3.script.
Using your text-editor window (as necessary) and REPL window, create a function named negate() that, given a sequence of numbers as its argument, returns a list containing each of those numbers negated. Your definition should not use recursion (nor loops!), nor should it use any named functions defined by you. To illustrate,
(negate [-1 2 -3]) (negate [4 -5 6])should return (1 -2 3) and (-4 5 -6), respectively.
When you have negate() working correctly, use our script+clojure procedure to make a recording 3a.script that shows your definition of negate() and demonstrates that it works correctly.
Use cat to verify that 3a.script is correct before proceeding.
Another function that takes a function as its argument is the reduce() function. This function takes another function, an optional argument, and a sequence (list, vector, etc) of arguments for it, and returns the result of applying that function to each of the arguments in the sequence.
In your REPL window, try these examples, and use our script+clojure procedure to make a file 4.script in which you record the output:
(reduce + [1 2 3]) (reduce + '(1 2 3)) (reduce max [1 2 3]) (reduce conj [1 2 3] [4 5 6])The pattern is as follows:
(reduce Function OptionalInitialValue Sequence)The function being applied must be applicable to the argument(s), and the last argument to reduce() must be a sequence. For example, try these:
(reduce + [1 2 3 4 5]) (reduce + 1 [2 3 4 5]) (reduce + 1 '(2 3 4 5)) (reduce max 5 [4 3 2 1]) (reduce bit-or [1 2 3 4]) (reduce str ["a" "b" "c"])You should discover that they are all valid. Now try these:
(reduce + 1 2 [3 4 5]) (reduce + 1 '(2 3 4) 5)They produce errors because they do not follow the pattern.
We can also use anonymous functions with reduce(). For example, suppose we have a vector of numbers, and we want to convert it to a string, in which the numbers are separated by commas (i.e. comma-separated-values, also known as the .csv format). We could do this with reduce() and a lambda function:
(defn csv [seq]
(reduce (fn [a b] (str a "," b)) seq)
)
Given this definition, what does the following produce?
(csv [11 22 33])
Lambda functions in combination with functions like map(), filter(), reduce(), and others thus provide a way to write very powerful operations in very concise notation.
The map(), filter(), and reduce() functions can also be combined. For example, suppose we have some sequences in vectors and/or lists, and we want to know the total number of values in all of them? Try this:
(defn totalItems [seqOfSeqs] (reduce + (map count seqOfSeqs)) ) (totalItems [["Ann"] ["Bob" "Chris"] ["Dan" "Eve" "Fred"]])What is happening?
First, the map() function applies the count() function to each of the sequences in seqOfSeqs. For the sequences given above, this produces a list (1 2 3).
The reduce() function then uses the + function to combine the numbers in that list, yielding 1+2+3 = 6!
The map(), filter(), and reduce() functions can thus be combined in amazingly powerful ways. As one example, as you continue your study of CS, you may encounter the phrase MapReduce, which is a technology developed at Google for processing data sets too big to fit on a single computer. You can probably guess from its name -- this technology was inspired by the LISP family's map() and reduce() functions!
Close your 4.script file and use cat to verify that its contents are correct before proceeding.
Using your text-editor window (as necessary) and your REPL window, build a function sumSquares() that, given a sequence as its argument, computes the sum of the squares of the numbers in that sequence. Your definition should not use recursion (nor loops!), nor should it use any named functions you have defined. (Hint: Think map-reduce.) Here are some test examples:
(sumSquares '(1 2 3)) ; should return 14 (sumSquares '(1 2 3 4 5)) ; should return 55 (sumSquares [-1 -2 -3 0 1]) ; should return 15When you have your function working, use our script+clojure procedure to make a recording 4a.script that shows your definition of negate() and demonstrates that it works correctly.
Use cat to verify that 4a.script is correct before proceeding.
Anonymous functions also let us write functions that return functions.
For example, if we were to write this:
(defn incMaker [incValue]
(fn [x]
(+ x incValue)
)
)
or even more succinctly, using the short-cut notation:
(defn incMaker [incValue] #(+ % incValue) )then we are defining incMaker() as a function that takes an argument via its parameter incValue, whose return-value is another function. That other function adds incValue to whatever other argument it receives.
Using our script+clojure procedure, start a recording 5.script for this part of the exercise. Choose one of the above definitions of incMaker() and paste it into the REPL.
Once we have incMaker() defined, we could use it to define an alternative version of inc() called inc5() that returns its argument plus 5, as follows:
(def inc5 (incMaker 5))Try that and then verify that inc5() will increment whatever argument is passed to it by 5.
Note that we have to use the def() function here -- not defn() -- because we are not defining a function in the usual way. Instead, we are binding the name inc5 to the function returned by incMaker(5).
Here is another example for you to try:
(defn greetingBuilder [greeting]
(fn [visitor]
(str greeting ", " visitor "!")
)
)
If we then create instances of greeterBuilder()
and bind them to descriptive names:
(def csGreeting (greetingBuilder "Welcome to CS 214")) (def englishGreeting (greetingBuilder "Hello")) (def frenchGreeting (greetingBuilder "Bonjour"))we can then write:
(csGreeting "Ann") (englishGreeting "Chris") (frenchGreeting "Chris")Verify that these all work before proceeding.
In each of these examples, the function-objects that incMaker() and greetingsBuilder() return are called closures. In fact, the name Clojure is a pun on the word closure, Clojure having been influenced by features from C#, LISP, and Java.
Quit from clojure and script; then use cat to verify that 5.script contains an accurate recording of this part of the exercise before proceeding.
At this point, the following (non-Clojure) humorous explanations of these functions hopefully make sense:
The functional language LISP was the first programming language to support lambda functions, and they have been a common feature of functional programming languages ever since.
However, lambda functions are increasingly common in modern mainstream languages. For example, C++ (as of C++11), C#, Go, Java (as of Java-8), Javascript, Lua, Python, R, Ruby, Scala, Swift, and Visual Basic .NET all support lambda / anonymous functions. Since most modern languages either support lambdas or have plans to add support for them in the near future, you will likely be using them if you become a software developer. Hopefully this exercise has provided you with the foundation needed to start using lambda functions in these and/or other languages.
In working through this exercise, you made a series of script files. Use cat to combine these into a single file:
cat 0.script 1.script 1a.script 1b.script 2.script 2a.script 3.script 3a.script 4.script 4a.script 5.script > lab05-resultsThen submit your work by copying that single file into your personal folder in /home/cs/214/current/:
cp lab05-results /home/cs/214/current/yourUserName
replacing yourUserName with your login name.
The grader will access and grade your results from there.
That concludes this exercise. If you wish, feel free to continue to this week's project.
Calvin > CS > 214 > Labs > 05 > Clojure Lambdas