Begin by creating a clojure directory in your 214/labs/07 directory, and a src directory inside the clojure directory. Then copy the program skeleton average.clj from the course directory into your new src directory. Use a text editor to open the average.clj source file, and take a few minutes to study it, to see how it implements our basic algorithm.
Clojure supports two kinds of arrays:
You may recall that we used a vector back in lab05 as means of having a function return two values. As we saw there, a vector can be constructed using the vector() function, and the get() function can be used to retrieve the value at a given index. These are two of many predefined functions Clojure provides for vectors; check out the vector section of the Clojure Cheatsheet.
A vector literal consists of a sequence of values surrounded by square brackets ([ and ]). In the -main() function of average.clj, the following lines use vector literals to define the emptyVec and testVec variables:
(let
[ emptyVec []
testVec [9.0 8.0 7.0 6.0]
]
...
)
Perhaps less obviously, the first argument of Clojure's let function
is a vector-literal of bindings.
Each binding defines a local variable by associating an identifier with a value.
Clojure's syntax is defined using Clojure!
The program skeleton in average.clj should build and run without errors, so take a moment to verify that it executes correctly before you proceed further.
Note that Clojure's print() and println() functions will correctly print a vector for us.
Note also that the -main() function contains lines that are commented out. We will be uncommenting these lines later in the exercise, after we have written the functions they call.
One of the subprograms we need to write is average(). To write this subprogram, we must pass a vector parameter and check it: if it is empty, return 0.0; otherwise compute the sum of the values in that vector and return that sum divided by the number of values in the vector.
Recalling the structure of a Clojure function definition:
FunctionDef ::= (defn identifier [Parameters]
Documentation
ExpressionList)
Parameters ::= identifier Parameters | Ø
Documentation ::= "Characters" | Ø
ExpressionList ::= Expression ExpressionList | Ø
Note that unlike our other languages,
LISP-family languages do not require any types to be specified
because no compile-time type-checking is performed;
instead, types are checked when arguments are passed at run-time.
Also, Clojure vectors know their sizes, making it unnecessary to pass the size of the vector to our average() function.
Using this information, create a stub for function average(), having a single parameter named aVec.
To guard against the user passing a non-vector argument to our function, we can adjust our algorithm slightly:
If aVec is a vector:
If aVec is empty:
return 0.0.
Otherwise
return the sum of the values in aVec /
the number of values in aVec.
We can check the first condition using the Clojure if and
vector? functions:
Expression ::= (vector? Object )The vector? function returns true if Object is a vector, and returns nil otherwise.
Clojure vectors are "smart", in that they know the number of items they contain. In situations like ours, Clojure's empty?() function can be used to determine if a vector contains any values:
Expression ::= (empty? Container )When Container is any container data structure (i.e., a vector, list, etc), the empty?() function returns true if the container contains no values, and returns false otherwise.
We can thus encode our second condition using an if function and the empty?() function. Take a moment to add these if, vector? and empty? function calls to your average() stub.
For the rest of the average() function, we can use the / operator to perform the division, and we can use the count() function to compute its denominator (the number of values in aVec):
Expression ::= (count Collection)Given a Collection object (i.e., a vector, a list, etc.), the count() function returns the number of values in that object.
That leaves us the problem of computing the numerator -- the sum of the values in aVec. Clojure has no built-in function to sum the values in a vector. so let's figure that out next.
We can specify the problem as follows:
Receive: aVec, a vector of numbers. Return: the sum of the numbers in aVector.Take a moment to use this information to create a stub for function sum() at the appropriate spot within average.clj.
There are a variety of ways to solve this problem in Clojure. We are going to look at two of them: a harder way and an easier way.
The Harder Way. The harder way is to solve the problem recursively. We would have to use this approach if Clojure did not provide an easier way, and since many languages do not provide the easier way, let's practice our recursive thinking skills and work through the logic.
Thinking recursively, we can identify this base-case:
Basis: aVec is empty:
-> return 0.0.
For the induction step, we can solve non-trivial cases this way:
I-Step: aVec is not empty.
-> return the last value in aVec + sum(aVec without its last item)
For safety, we should check that aVec is indeed a vector.
we can consolidate all of these thoughts into this algorithm:
If aVec is a vector:
If aVec is empty:
return 0.0.
Otherwise
return the last value in aVec + sum(aVector without its last value).
As we have seen before,
we can use the if and vector? functions
to determine if aVec is a vector.
Likewise,
use another if and empty?() functions
to implement the nested-if step.
Take a moment to add this much logic to your sum() stub.
Having that nested if return 0.0 is easy, so add that to your stub.
That leaves us with two challenges:
Expression ::= (peek VectorObject)Given a vector argument, the peek() returns its final value.
We can compute a vector without its last value using the pop() function:
Expression ::= (pop VectorObject)Given a vector argument, the pop() function returns that vector without its final value.
Combining all of these observations, we might define sum() as follows:
;; harder (recursive) solution
(defn sum [aVec]
(if (vector? aVec) ; if aVec is a vector
(if (empty? aVec) ; if aVec is empty:
0.0 ; return 0
(+ (peek aVec) ; else return the last value
(sum (pop aVec)) ; + sum(all but the last value)
)
)
)
)
In average.clj, make your definition of sum() consistent with the one above. Make sure that you understand how it works before continuing.
Finally, use our new sum() function to complete function average(). Then uncomment the lines in -main() that display the results of sum() and average(). Save your changes, run the program, and verify that it works as expected. Continue when this much is working correctly.
The Easier Way. The easier way to define a function to sum the values in a vector is as follows:
;; easier (non-recursive) solution
(defn sum2 [aVec]
(if (vector? aVec) ; if aVec is a vector:
(if (empty? aVec) ; if aVec is empty:
0.0 ; return 0
(reduce + aVec) ; else reduce aVec using +
)
)
)
Aside from its name,
this version only differs from our first version
in how it sums the numbers in a non-empty vector.
It does so using the LISP-family reduce() function,
which has the following syntax:
Expression ::= (reduce Operator Collection)Given a commutative Operator and a Collection data structure, the reduce function returns the result of using Operator to combine the values in Collection. Thus, for non-empty vector objects, the line:
(reduce + aVec)will sum all the numbers in aVec for us!
Copy-paste the definition of sum2() into your source file, below the definition of sum(). Then uncomment the lines in the -main() function that test sum2(). Save your changes and verify that sum() and sum2() produce the same results, before continuing.
When average.clj is all finished, use script to create a new script.clojure file in which you use cat to list the source file, show that it compiles and runs without errors, and produces correct results. Quit script and move the script.clojure file "up" to your clojure folder's parent folder (with the other script files).
That concludes the Clojure portion of this lab.
Calvin > CS > 214 > Labs > 07 > Clojure