January 16, 2021

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.

Tags: Clojure