Begin by creating a clojure directory inside 214/labs/10/; then create a src directory inside clojure, and copy the files birds.clj, Bird.clj, Duck.clj, Goose.clj, and Owl.clj from the course labs/10/clojure/ directory into your src directory. There is also a Makefile in the course directory; if you wish to use make to build and run your program, copy it into your clojure directory.
Using a text editor, open birds.clj and Bird.clj and take a few minutes to explore the files to get a sense of how they relate to one another. Then build and run birds.clj to see how the program behaves at the outset.
One new thing in birds.clj occurs in our ns() (namespace) function:
(ns birds
(:require
[Bird]
[Duck]
[Goose]
[Owl]
)
)
(Some of these are commented out; leave them that way for now,
as we'll be uncommenting them later in this exercise.)
When a namespace depends on externally-defined modules, the (:require ...) clause can be used to explicitly state this dependency. When the Clojure compiler processes this clause, it will load and process the contents of those modules for us, so this eliminates the need to write:
(load "Bird") (load "Duck") (load "Goose") (load "Owl")
Clojure is not really an object-oriented language. Rather, it is a functional language that supports some object-oriented features. For example, Clojure doesn't really have a way to designate an O-O superclass-subclass relationship. But as we shall see, Clojure does provide its own version of polymorphism. (It also provides a mechanism called protocols for creating abstract interfaces, though that is beyond the scope of this exercise.)
In the file Birds.clj, the function call:
(defrecord Bird [name])creates a new record-type named Bird, with one field named name. We used defrecord in a previous exercise, so there's nothing new here.
Defining the make-Bird() Function. The program in birds.clj invokes a make-Bird() function to build and initialize Bird objects:
(let
[
bird0 (make-Bird) ; default Bird constructor
bird1 (make-Bird "Hawkeye") ; explicit Bird constructor
...
These calls initialize bird0 by invoking
make-Bird() with no arguments
-- the equivalent of a default constructor --
and initialize bird1
by invoking make-Bird() with a string argument
-- the equivalent of an explicit constructor.
In order for these calls to work, we need to supply a
definition for make-Bird().
One way to do so is as follows:
(defn make-Bird ([] (->Bird "Ann Onymous")) ([itsName] (->Bird itsName)) )This one definition of make-Bird() supports two different behaviors:
Find the right spot and copy-paste this function definition into Bird.clj. Then in birds.clj, uncomment the lines that call make-Bird().
Save your changes; then rebuild and run birds.clj to verify that what you have so far is free of syntax errors.
Defining An Accessor. Since our Bird has a name field, we might declare an accessor function for this field. Doing so is pretty easy, as we have seen before:
(defn getName [^Bird this] (:name this) )The one thing that is slightly different is that in this getName() accessor function, we have used a compiler-hint to specify Bird as a type for our parameter this. We do this because the body of the function uses :name() to retrieve the value of the name field, and only Bird objects have such a field.
Find the right spot in Bird.clj and copy-paste this definition there. Save your changes; then rebuild and run birds.clj to verify that what you have added is free of syntax errors.
Polymorphism In Clojure. Since a Bird has a name field, we would not expect subclasses of Bird to want to override getName() with new definitions.
By contrast, each subclass will need to provide its own definitions for our getClass() and getCall() functions in order for those functions to work correctly. Likewise, a subclass may want to provide its own definition of a toString() function, so we should allow for that. Put differently, getClass(), getCall(), and toString() should all be polymorphic functions, but getName() does not need to be polymorphic.
Clojure calls a polymorphic function a multimethod. Declaring a multimethod involves two steps:
(defmulti getClass class) (defmulti getCall class) (defmulti toString class)These lines specify getClass, getCall, and toString as methods on a class, meaning calls to these methods may be dispatched differently, depending on the class/type on which they are being invoked. (Clojure allows methods to be invoked and dispatched on things besides types, such as functions.)
Find the right spot in Bird.clj and copy-paste these lines there, before proceeding.
To define getClass(), getCall() and toString() as multimethods, we use the defmethod() function, as shown below:
(defmethod getClass Bird [ _ ] "Bird" ) (defmethod getCall :default [ _ ] "Squaaaaawk!" ) (defmethod toString :default [aBird] (str (getName aBird) " " (getClass aBird) " says, \"" (getCall aBird) "\"") )The first definition defines getClass() as a method of class Bird. The method has a single parameter named _; in methods like this where we want the user to pass an argument but the method doesn't use that argument, _ can be used as the name of the parameter. As appropriate for this version of getClass(), the method returns the string "Bird".
The second definition defines getCall() as the default definition (for any subclass that does not redefine it), again with a single parameter _, and that returns the string "Squaaaaawk!".
The third definition defines toString() as the default definition (for any subclass that does not redefine it), with a single parameter aBird, and that returns the string consisting of concatenated calls to getName(), getClass(), getCall(), and some other strings.
Find the right spots in Bird.clj and copy-paste these method definitions into those spots, and save your changes before proceeding.
Testing "Class" Bird. In birds.clj uncomment the calls to println() that display the string representations of bird0 and bird1.
Save these changes; then build and run the program. If you have done everything correctly, you should see something like this:
Welcome to the Bird Park! Ann Onymous Bird says, "Squaaaaawk!" Hawkeye Bird says, "Squaaaaawk!" Goodbye, and come again!
In our design, Duck is a subclass of Bird, so open the file Duck.clj. For Duck, we have supplied all of the code, so let's go through it and see what each function is doing.
Making Bird Visible. In order for Duck.clj to be able to "see" our Bird class, it needs to load our Bird module. The following line (near the top of Duck.clj) does this:
(load "Bird")
Defining Duck. To try to declare Duck as a "subclass" of Bird, we can write:
(defrecord Duck [^Bird name] )By providing a compiler-hint with the field-name, we can stipulate that Duck has a single field named name, but that it is the name field of a Bird. (In my experience, it appears that the field-name in the "subclass" has to be the exact same identifier as field-name in the "superclass" in order for this to work properly.)
It is important to point out that with this notation, we are not stating that a Duck is-a Bird; we are instead stating that a Duck has-a Bird field. As we noted earlier, Clojure isn't really an object-oriented language; this seems to be as close as we can get to a subclass-superclass relationship in the current version of the language.
Don't do this, but note that if we wanted to add another field to our Duck record-type, such as its color (e.g., white, black, brown, speckled, ...), we could easily do so:
(defrecord Duck [^Bird name color] )Then we would have to define our Duck constructors to allow us to initialize this field, provide an accessor for it, and redefine toString() to incorporate it. Our design doesn't call for this, so we won't take the time to it, but doing so is not difficult.
Duck Constructors. To initialize a Duck, we use a constructor similar to the one we used for Bird:
(defn make-Duck ([] (->Duck "Ann Onymous")) ([^String itsName] (->Duck itsName)) )This one function definition supports the equivalent of a default constructor (the one with no parameters) and the equivalent of an explicit constructor (the one with one parameter).
The Duck Multimethods. Of our three multimethods, Duck only needs to override two of them: getClass() and getCall(), which it does as follows:
(defmethod getClass Duck [ _ ] "Duck" ) (defmethod getCall Duck [ _ ] "Quack!" )These two functions provide class-appropriate definitions of the getClass() and getCall() methods. Note that we must specify the type Duck in order for these methods to be invoked for Duck objects.
That's it! Back in birds.clj, uncomment the Duck-related calls. (Don't forget the one in the :require near the top.)
Save your changes; then rebuild and run to verify that these Duck functions and methods work as expected.
Note that in birds.clj, the lines:
(println (toString bird2))
(println (toString bird3))
invoke the toString() function on Duck objects,
but we did not define this function in Duck.clj.
As can be seen when you run the program,
the definition of toString() from Bird.clj
is being used, but when it calls getClass() and
getCall(), it uses our Duck definitions
of those functions instead of the Bird definitions
-- polymorphic behavior!
In our design, Goose is another subclass of Bird, so open the file Goose.clj.
Making Bird Visible. In order for Goose.clj to be able to "see" our Bird class, it needs to load our Bird module. Add the following line to the appropriate place (near the top) of Goose.clj):
(load "Bird")
Defining Goose. To declare Goose as a kind of Bird, we can write:
(defrecord Goose [^Bird name] )Add this line to the appropriate place in Goose.clj.
Goose Constructors. To initialize a Goose, we use a constructor similar to the one we used for Bird and Duck:
(defn make-Goose ([] (->Goose "Ann Onymous")) ([^String itsName] (->Goose itsName)) )Add this definition to the appropriate place in Goose.clj.
The Goose Multimethods. Of our three multimethods, Goose only needs to override getClass() and getCall():
(defmethod getClass Goose [ _ ] "Goose" ) (defmethod getCall Goose [ _ ] "Honk!" )Add these two definitions to Goose.clj in the appropriate places.
Back in birds.clj, uncomment the Goose-related lines. (Don't forget the one in the :require near the top.)
Save your changes; then rebuild and run to verify that these Goose functions and methods work as expected.
As before, note that the calls to our getClass() and getCall() multimethods in toString() are invoking different methods, depending on whether they are passed a Bird, a Duck, or a Goose object!
In our design, Owl is our final subclass of Bird, so open Owl.clj. Using Duck and Goose as models, add the code needed so that Owl objects behave as expected in birds.clj.
Uncomment the lines in birds.clj that refer to Owl objects and test what you have written. Continue when your code in Owl.clj works correctly.
Use script to create a script.clojure file that lists the contents of both birds.clj, Bird.clj, Duck.clj, Goose.clj, and Owl.clj, and that demonstrates that your program builds and runs correctly. Quit script.
That concludes the Clojure part of this lab.
If this was your last part of the exercise, return to the lab 10 page and follow the "Turn In" instructions there.
Calvin > CS > 214 > Labs > 10 > Clojure