Learning Clojure - Implementing cat
As I wrote previously, I am learning the Clojure programming language as one of my personal Learning Projects.
Previously, I presented my implementations of echo
, the classic Unix command-line utility. Next on the list is cat
, a useful utility for writing information out to the screen from a file or the standard input. Well, technically it writes to the standard output, but the default destination for the standard output is the screen, so it works out the same in the end.
Now, let's take a look at the code:
(ns cat.core
(:gen-class))
(use 'clojure.java.io)
(defn readAndPrintFile [name]
(with-open [rdr (reader name)]
(doseq [line (line-seq rdr)]
(println line))))
(defn readAndPrintStdin []
(doseq [ln (line-seq (java.io.BufferedReader. *in*))]
(println ln)))
(defn -main
"Simple implementation of the Unix cat utility."
[& args]
(if (= (count args) 0)
(readAndPrintStdin)
(doseq [a args]
(readAndPrintFile a))))
I think that this is my first real effort at dividing up the code into separate functions. I could have left everything in -main
, but then it would have been much more complicated, less easy to read and I strongly suspect that it would have been considered poor style. Clojure has a preference for functions to be defined before use (blame a single pass compiler for that), although it is possible to declare functions first and define them later if you wish to get around this rule. I generally prefer to start with my main
function, but I've used enough other languages with similar requirements that I don't fight it any more and just roll with the most idiomatic way of writing any particular language.
The -main
function is nice and straight-forward. If the program receives no arguments on the command line, then it assumes it is reading from the standard input, otherwise it treats each parameter as the name of file that it needs to read and display the contents of, in the order that they are received. I have tried to make this clear with the names of the two methods that get called depending upon the count of the parameters received through the command line. Sending the filenames from the command-line parameters is achieved by use of the doseq
function, which takes a var, a sequence (in this case a list) and a function and applies the supplied function to each of the elements of the list in order.
After lots of searching (or as some users of Duck Duck Go call it ... ducking) I found enough example code to compose the readAndPrintStdin
function. The inner expression gives us a BufferedReader
attached to the standard input. The line-seq
expression takes that and returns a lazy sequence of strings. Lazyness (in functional languages) is a whole topic in itself, so I'll skip it for now, which is itself lazy ... see what I did there!? Finally, the returned line of text is assigned to the var
named ln
(because line
would have been far too long an identifier name) and then it is used by the following expressions, of which there is precisely one and it is our old friend println
who promptly prints the line of text. The doseq
expression repeats it's body for as long as new data is returned, after which it stops and returns with nil
.
The readAndPrintFile
function is a slightly more general case of the readAndPrintStdin
function in which a filename is supplied and then that file is opened and it's contents read and printed. The main difference is with the reader object that is created to represent the named file. The with-open
function is a convenience function that guarantees to close the file you ask it to open. This enables the programmer to concentrate more on what they want to do to the file rather than taking extra time and code to open it, process it and then close it. A useful convenience function.