Programming to Abstractions

Clojure’s spit and slurp I/O functions are built on two abstractions, reading and writing. This means you can use them with a variety of source and destination types, including files, URLs, and sockets, and they can be extended to support new types by anybody, whether they’re existing types or newly defined.

  • The slurp function takes an input source, reads the contents, and returns it as a string.

  • The spit function takes an output destination and a value, converts the value to a string, and writes it to the output destination.

We’ll start by writing basic versions of the two functions that can read from and write to files only. We’ll then refactor the basic versions several times as we explore different approaches to supporting additional datatypes. Working through this will give you a good feel for the usefulness of programming to abstractions in general and the flexibility and power of Clojure’s protocols and datatypes in particular.

After writing our versions of spit and slurp, called expectorate and gulp, respectively, which work with several existing datatypes, we’ll create a new datatype, CryptoVault, which can be used with our versions of the functions as well as the originals.

The gulp function is a simplified version of Clojure’s slurp function, and expectorate, despite its highfalutin name, is a dumbed-down version of Clojure’s spit function. Let’s write a basic version of gulp that can read from a java.io.File only.

 (ns examples.gulp
  (:import (java.io FileInputStream InputStreamReader BufferedReader)))
 (​defn​ gulp [src]
  (​let​ [sb (StringBuilder.)]
  (with-open [reader (-> src
  FileInputStream.
  InputStreamReader.
  BufferedReader.)]
  (loop [c (.read reader)]
  (​if​ (neg? c)
  (str sb)
  (do
  (.append sb (char c))
  (recur (.read reader))))))))

The gulp function creates a BufferedReader from a given File object and then loops/recurs over it, reading a character at a time and appending each to a StringBuilder until it reaches the end of the input where it returns a string. The basic expectorate function is even smaller:

 (ns examples.expectorate
  (:import (java.io FileOutputStream OutputStreamWriter BufferedWriter)))
 
 (​defn​ expectorate [dst content]
  (with-open [writer (-> dst
  FileOutputStream.
  OutputStreamWriter.
  BufferedWriter.)]
  (.write writer (str content))))

It creates a BufferedWriter file, converts the value of the content parameter to a string, and writes it out to the BufferedWriter.

But what if we want to support additional types like Sockets, URLs, and basic input and output streams? We need to update gulp and expectorate to be able to make BufferedReaders and BufferedWriters from datatypes other than files. So, let’s create two new functions, make-reader and make-writer, that will be responsible for this behavior.

  • The make-reader function makes a BufferedReader from an input source.
  • The make-writer makes a BufferedWriter from an output destination.
 (​defn​ make-reader [src]
  (-> src FileInputStream. InputStreamReader. BufferedReader.))
 
 (​defn​ make-writer [dst]
  (-> dst FileOutputStream. OutputStreamWriter. BufferedWriter.))

Like our basic gulp and expectorate functions, make-reader and make-writer work only on files, but that will change shortly. Now let’s refactor gulp and expectorate to use the new functions:

 (​defn​ gulp [src]
  (​let​ [sb (StringBuilder.)]
  (with-open [reader (make-reader src)]
  (loop [c (.read reader)]
  (​if​ (neg? c)
  (str sb)
  (do
  (.append sb (char c))
  (recur (.read reader))))))))
 (​defn​ expectorate [dst content]
  (with-open [writer (make-writer dst)]
  (.write writer (str content))))

We can now add support for additional source and destination types to gulp and expectorate just by updating make-reader and make-writer. One approach to supporting additional types is to use a cond or condp statement to process different types appropriately. For example, the following version of make-reader replaces the call to the FileInputStream constructor with a condp statement that creates an InputStream from the given input, whether it’s a File, Socket, or URL or already is an InputStream.

 (​defn​ make-reader [src]
  (-> (condp = (type src)
  java.io.InputStream src
  java.lang.String (FileInputStream. src)
  java.io.File (FileInputStream. src)
  java.net.Socket (.getInputStream src)
  java.net.URL (​if​ (= ​"file"​ (.getProtocol src))
  (-> src .getPath FileInputStream.)
  (.openStream src)))
  InputStreamReader.
  BufferedReader.))

Here’s a version of make-writer using the same strategy:

 (​defn​ make-writer [dst]
  (-> (condp = (type dst)
  java.io.OutputStream dst
  java.io.File (FileOutputStream. dst)
  java.lang.String (FileOutputStream. dst)
  java.net.Socket (.getOutputStream dst)
  java.net.URL (​if​ (= ​"file"​ (.getProtocol dst))
  (-> dst .getPath FileOutputStream.)
  (throw (IllegalArgumentException.
 "Can't write to non-file URL"​))))
  OutputStreamWriter.
  BufferedWriter.))

The problem with this approach is that it’s closed: nobody else can come along and add support for new source and destination types without rewriting make-reader and make-writer. What we need is an open solution, one where support for new types can be added after the fact and by different parties. What we need is two abstractions, one for reading and one for writing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.205.99