CS 214 Lab 8: Clojure


As usual, begin by making a clojure directory inside your 08 directory, and then making a src directory inside clojure. Copy the program skeleton nameTester.clj from the course directory into your new src directory. Then use a text editor to open the file, and take a moment to study its structure. Note that the -main() function uses assert() function calls to automate the testing of the functions we will be writing.

Representing a Name. One of the differences between Clojure and traditional LISP is that Clojure offers a variety of modern mechanisms for creating named aggregate types. For situations where we want to aggregate different data values in the type, Clojure lets us create a record type, and then define operations on that type. For example, if we need to model a name, consisting of a first-name, middle-name, and last-name:

   "John" "Paul" "Jones"
Then we can define a record containing three fields, one for each name-component. However, Clojure is a functional language, not an object-oriented language. As such, it doesn't really provide a construct (i.e., class) for explicitly encapsulating both data and functionality in the OO sense. Instead, we will have to write "external" functions that, given a record representing a name, will perform the appropriate operation on that name.

Getting Started

Start by running the program in nameTester.clj. What happens?

Defining a Record Type

To represent 3-part names, we need to define a record-type named Name. To define a record type, Clojure provides the defrecord function, which has the following form:

   DefRecFunction   ::= (defrecord identifier [ IdList ])
   IdList           ::= identifier IdList | Ø
When the defrecord() function is executed, it creates a new type whose name is the identifier, and whose fields have the names listed in the IdList.

For example, if we wanted to create a new record-type named Point, with fields x and y, then we could write:

   (defrecord Point [ x y ] )
Note that we do not specify any type information; we simply indicate the names of the fields.

Using this as a model, find the line in nameTester.clj that looks like this:

; Replace this line with the definition of record-type Name
and replace it with a line that defines a record-type named Name, with fields firstName, middleName, and lastName.

Run your program again and make certain the code you have added builds and runs correctly before proceeding.

Before we proceed further, it is worth mentioning that the Clojure compiler actually compiles such record definitions into Java classes. To keep things simple, we are not using the full power this offers, but keep this in mind going forward.

Initialization

In LISP-family languages, the tradition is to perform initialization using a "make-X" function, where X is the type of the thing being initialized. For example, to initialize a Point object in Clojure, we might define a make-Point() function as follows:

   (defn make-Point [xVal yVal]
      (->Point xVal yVal)
   )
When executed, this function accepts two arguments via parameters xVal and yVal, and passes them on to a ->Point() "factory function", the the Clojure compiler creates when it executes a defrecord() function. This "factory function":
  1. constructs a Point object,
  2. initializes its x field to xVal,
  3. initializes its y field to yVal, and
  4. returns the resulting Point object.
Given our make-Point() function, a let() function could then use it to initialize Point objects:
   (let
      [ p1     (make-Point 0.0 0.0)
        p2     (make-Point 1.2 3.45)
      ]
      ...

In function -main(), find and uncomment the following line:

  ; name1 (make-Name "John" "Paul" "Jones")  ; -using our "make-" constructor
Then run nameTester.clj again. What happens?

To make this work, use the preceding information to write a function named make-Name with three appropriately-named parameters (e.g., first, middle, and last), and uses those parameters to initialize the three fields of a Name. Save your changes; then rebuild and run your program to test what you have written. Continue when no errors are produced.

Using the Factory Function. You may be asking, since our make-Name() function used the ->Name() factory function, why can't we just use that factory function directly? The answer is, we can!

To illustrate using our Point type, a let() function could use the following to initialize Point objects:

   (let
      [ p1     (->Point 0.0 0.0)
        p2     (->Point 1.2 3.45)
      ]
      ...

Our nameTester.clj program already contains such an initialization. In the -main() function, find and uncomment the line:

;      name2 (->Name "Jane" "Penelope" "Jones") ; -invoking constructor directly
Save your changes; then rebuild and run your program. If all is well, this line should work correctly without any further changes, thanks to (defrecord Name ...).

Using map->X. When it executes a defrecord() function, the Clojure compiler also creates another factory function that provides us with a third mechanism for initialization. This mechanism combines the built-in map() function with the ->X() factory function. It lets us map initialization values directly to field-names, and so initialize the fields in whatever order one desires.

To illustrate, a let() function could use this form to initialize Point objects as follows:

   (let
      [ p1     (map->Point {:x 0.0 :y 0.0} )
        p2     (map->Point {:y 3.45 :x 1.2} )
      ]
      ...
Our nameTester.clj program already contains such an initialization. In the -main() function, find and uncomment the line:
;      name3 (map->Name {:lastName "Jones" :firstName "Jinx" :middleName "Joy"})
Save your changes; then rebuild and run your program. If all is well, this line should work correctly without any further changes. Continue when this is the case.

Output, v1

To test that our Name initialization function is working correctly, we can try to output our Name objects.

In nameTester.clj, the rest of the -main() function consists of three sections: one that tests name1, one that tests name2, and one that tests name3. In each of these sections, uncomment the println() and print() calls at the beginning of the section.

Save these changes, build, and run your program. Clojure will display its representation of each of our three Name objects.

Clojure treats records as immutable data structures that map field names to values. As a result, the default format for outputting a record-type is to display the namespace containing that type, the name of the record-type, and for each field within it: the name of the field followed by its value. This is good for debugging since it lets us see all of the information in a given object.

Compare the displayed information against what your specified when initializing name1, name2, and name3. When your have verified that each of your constructors is working correctly, continue.

Accessors

Since our Name record stores values in fields, it would be useful to have accessor functions to retrieve the values of those fields, so let's write them next. Uncomment the first assert() function call in each of these sections. Save your changes and then rebuild and run your program. What happens?

Defining getFirst(). To fix this, we need to define a getFirst() accessor function for our Name record-type.

In nameTester.clj, find the following line:

; Replace this line with the definition of getFirst()
and replace it with a stub definition for getFirst(). Your stub should have a single parameter (i.e., aName), as indicated in the function specification.

In this situation, we do not want arbitrary objects to be passable to our getFirst() function -- we want to limit things so that only Name objects can be passed. In these situations, you can provide a compiler hint that tells the clojure compiler to reject non-Name arguments.

To illustrate using our Point class, we might create the following stub for a getX() accessor function:

   (defn getX [^Point aPoint]
     ; ToDo: complete this function
   )
By placing ^Point before parameter aPoint, we tell the compiler to only accept Point arguments for this function.

Using that as a model, add a compiler hint to your getFirst() stub, so that the compiler will only accept Name arguments in calls to getFirst().

To complete the stub, we need to retrieve the value of a field from a record. To see how to do this, let's return once again to our Point example. To complete the getX() function for a Point, we could write:

   (defn getX [^Point aPoint]
     (:x aPoint)
   )
That is, Clojure supports the syntax
   (:fieldName objectName)
to retrieve the value stored in fieldName within objectName.

Using this as a model, complete your definition of your getFirst() function. Save your changes, rebuild, and run your program to test what your have written. Continue when your getFirst() function passes the three tests in -main().

Defining getMiddle(). Next, uncomment the second assert() function in each of the three sections. Save, rebuild, and run your program. What happens?

Find the line:

; Replace this line with the definition of getMiddle()
Using what you wrote for getFirst() as a model, implement the getMiddle() accessor function.

As before, save, rebuild, and run your program to test your work. Continue when your function passes all three tests.

Defining getLast(). Next, uncomment the third assert() function in each of the three sections; save, rebuild, and run your program. What happens?

Using what you have learned so far, add a getLast() function that retrieves the value of the lastName field. Continue when all three accessors are correctly passing their tests.

Mutators

For future reference, it is worth mentioning that unlike our other languages, Clojure records are immutable data structures. This means that we cannot define mutators (i.e., functions that change the value of an object's field) in the usual sense. Instead, a "mutator" function must build and return a new copy of the record in which all the fields are the same except for the field being mutated, which gets the new value.

To illustrate using our Point class, we might define a setY() mutator as follows:

(defn setY [aPoint newY]
  (->Point (:x aPoint) newY)
)
This function has two parameters aPoint and newY, and builds and returns a new Point whose x field is the same as that of aPoint but whose y field is newY.

A let() function might use such a mutator something like the following:

  (let
    [ p1 (->Point 0.0 0.0) 
      p2 (setY p1 1.5)
    ]
    ...
The object p2 will be a mutated version of p1 in which y has the value 1.5 instead of 0.0.

String Conversion

Being able to convert an object to a string representation can be useful for a variety of purposes, so let's do that next.

Uncomment the fourth assert() function in each section. Save your changes, rebuild, and run your program, to verify that the code fails these tests.

To pass the test, we must define a toString() function that converts a Name object into a string. Find the following line in nameTester.clj and replace it with a stub for toString():

; Replace this line with a definition of toString()
(Don't forget the compiler hint!)

Since the fields of our Name type are all strings, completing the definition of toString() consists of concatenating and returning the three fields of a Name, separated by spaces. To perform the concatenation, we can use the str function:

   Expression        ::=   (str ExpressionList )
   ExpressionList    ::=   Expression MoreExprs
   MoreExprs         ::=   Expression MoreExprs | Ø  
Given a sequence of expressions, the str function concatenates them together into a single string and returns that string.

Using the str function and your three accessor functions, complete the definition of toString() so that, given a Name object, its returns the three fields of that object (separated by spaces) as a single string.

Save your changes, rebuild, run your program, and verify that toString() passes the tests. Continue when it does.

Output, v2

As we have seen, Clojure's default format for outputting a record provides lots of information that is useful for debugging. But what if we just want the values stored in a record's fields, without all of the other information?

To provide nicer output for the average human, we can write a function that, given a Name object, prints the result of calling our toString() function on that object. Thanks to our toString() function, this is quite easy -- we'll call this function printName() to keep it distinct from the standard output functions.

In each of the three sections, uncomment the call to printName() at the end of the section. Above the -main() function, find the line:

; Replace this line with a definition of printName()
and replace it with a definition of a printName() function that, given a Name object, uses print() and toString() to display our string representation of that object. (Don't forget the compiler hint!)

At this point, all of the key lines of function -main() (i.e., those that test our functions) should be uncommented. Double-check that this is the case; we want to be able to see and compare the calls to the standard print() function and our printName() function for each of our Name objects.

Save any changes; rebuild, and run the program. Continue when everying works correctly.

Wrapping Up

When nameTester.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 part of this lab.


Calvin > CS > 214 > Labs > 08 > Clojure


This page maintained by Joel Adams.