In your 214/labs/09 directory, create an clojure directory; then create a src directory inside clojure. Then copy the files nameTester.clj and Name.clj from the class labs/09/clojure directory into your src directory.
(It isn't necessary, but for your convenience, we have also provided a Makefile in the clojure course directory. All the Makefile does is run clojure -m nameTester.clj, but using it, you can build and run your program just by entering make on the command-line in your Clojure project directory. Using it can save you some typing, but not that much. If you want to use it, copy it into your labs/09/clojure directory, not your src directory.)
Using a text editor, open the two source files, and take a moment to study their contents. Customize their opening documentation sections, save them, and run the code in nameTester.clj to verify that it works correctly before proceeding.
As we saw last time, Clojure provides a record-type as an aggregate data structure. Like C, Clojure does not provide a distinct module construct, but relies upon the file system for modularity.
Code Reusability. To make Clojure code reuseable, we can simply place it in a separate file. As we shall see, any other file that wants to use that code can do so. Clojure thus successfully achieves the code reuseability goal of modularity.
Information Hiding. Clojure provides no mechanism for keeping information private: everything declared globally in a separate Clojure file is public, and the value of a field of a record can always be accessed via the field-accessor mechanism we saw last week:
(:fieldName objectName)One of the goals of information-hiding is to hide implementation details, so that programmers cannot write programs that depend on those details. (If they do so and those implementation details change, then those programs must also be changed, driving up software maintenance costs.) Clojure thus does not give us any good way to hide implementation details.
However, another goal of information-hiding is to prevent programmers from changing an object in a way that leaves it in an invalid state. Clojure (and other functional languages) do achieve this goal by making an object's state immutable: once an object has been constructed, its state cannot be changed, so if we ensure (when it is constructed) that an object is valid, programmers are unable to subsequently change that an object and make it invalid.
Clojure's module mechanism thus only partially achieves the information-hiding goal of modularity.
We have seen that Clojure lets us to define a Name record-type, and then define functions that provide operations on that type.
To make our Name type and its operations reuseable, simply cut them from where they reside in nameTester.clj and paste them into a separate (appropriately named) file -- Name.clj. (Leave the -main() function in nameTester.clj, to serve as a "driver program" to test our module.)
That's it -- Name.clj is now the Clojure equivalent of a module!
Once we have created a Clojure module, there are several ways to use it. The most straightforward are using either one of the load() or the load-file() functions:
Expression ::= (load ModuleName) ModuleName ::= "identifier" Expression ::= (load-file PathToFile) PathToFile ::= "Characters"
Using load(). Given the name of a module (e.g., Name, with no extension), the load() function looks in the src directory for that module. Upon finding it, it reads and processes the contents of that module. To use this approach and load our Name module (from the file src/Name.clj) we would just write the following in nameTester.clj, in place of the functions we cut-and-pasted:
(load "Name")Using the preceding information, add a call to load() to nameTester.clj to load the Name module. Once you have done this, the tests in nameTester.clj should all work without modification.
Save your changes, build and run nameTester, and if you have done everything correctly, it should run and test your functions without any errors. Continue when this is the case.
Using load-file(). In contrast to load(), the load-file() function requires us to provide: (i) the explicit path to the file we want to load, relative to our project folder; and (ii) the full name of the file, including its extension. Upon finding the file, this function reads and processes its contents, similar to the #include directive in C/C++.
To use this approach with our Name module, we would write:
(load-file "src/Name.clj")
In nameTester.clj, comment out the call to function load(). Beneath that commented-out call, add a call to load-file() to load the src/Name.clj file.
Save your changes, build and run it. If you have done everything correctly, your program should run and test your functions without any errors. Continue when this is the case.
Clojure's module mechanism achieves the code reusability goal of modularity. It doesn't really achieve the information-hiding goal (in the sense of hiding implementation details). But because Clojure objects are immutable, it does prevent programmers from changing an object's valid state to an invalid state.
Clojure's load() function is the simpler of the two mechanisms, in that it only requires us to specify the name of a module. It is limited though, in that the module must be in our src folder.
Clojure's load-file() function is more powerful, in that it can be used to load a file from anywhere in the computer's file system. That additional power makes it slightly more complicated to use.
When you have executed nameTester.clj correctly with both load() and load-file(), use script to create a new script.clojure file in which you use cat to list both source files, e.g.:
cat src/nameTester.clj src/Name.cljand show that they compile and run without errors. Then quit script.
That concludes the Clojure part of this lab.
Calvin > CS > 214 > Labs > 09 > Clojure