Note: There are a number of things that may be unfamiliar to new cljs devs. Rather than making this a huge post, I’ll write a separate post for each concept. For example, if the lazy-cat used in this post is unclear, check out my explanation of lazy-seq and recursion.

In my last post, we had some code that could read files line by line. We then used each line, either by a callback or a channel. While the code worked, I didn’t want the asynchronous reads. Instead, I wanted to get as close to this as possible:

(with-open [rdr (open-file path)]
(doseq [line (line-seq rdr)]
(js/console.log line))) ;;do something with line

This has the following benefits:

  1. It’s exactly the same as clj
  2. I don’t have to think about async code that provides no benefits here
  3. It lazily reads in the file and handles cleanup
  4. It’s a sequence and I can program to that interface

So the first thing was to switch from createReadStream to readSync. readSync synchronously reads in a user specified number of bytes.

(def fs (js/require "fs")) ;;require nodejs lib

(defn- read-chunk [fd]
(let [length 128
b (js/Buffer. length) ;;Buffer is a global nodejs lib
bytes-read (.readSync fs fd b 0 length nil)]
(if (> bytes-read 0)
(.toString b "utf8" 0 bytes-read))))

We can now read an arbitrary amount of data from the file, but we have to manage finding actual lines of text. Here’s the logic we need to implement:

  1. Get the next chunk of data
  2. If it contains a line of text, return that line of text and save the remaining text for the next call
  3. If it does not contain a line of text, get more data and try our line check again
  4. If we read the EOL, just return whatever data we have

Here’s what I ended up with:

(defn line-seq
(line-seq fd nil))
([fd line]
(if-let [chunk (read-chunk fd)]
(if (re-find #"\n" (str line chunk))
(let [lines (clojure.string/split (str line chunk) #"\n")]
(if (= 1 (count lines))
(lazy-cat lines (line-seq fd))
(lazy-cat (butlast lines) (line-seq fd (last lines)))))
(recur fd (str line chunk)))
(if line
(list line)

line-seq takes a file descriptor and lazily reads from the file, returning a lazy sequence of lines.

The last bit is our with-open macro. Note: cljs macros have to be written in clojure

(defmacro with-open [bindings & body]
(assert (= 2 (count bindings)) "Incorrect with-open bindings")
`(let ~bindings
(do ~@body)
(.closeSync cljs-made-easy.line-seq/fs ~(bindings 0))))))

Now, we’re where we want to be:

(with-open [fd (.openSync fs "cljs-love.txt")]
(doseq [line (line-seq fd)]
(js/console.log line))) ;;do something with line

And since we have a seq, we can use all of our normal tools:

(with-open [fd (.openSync fs "cljs-love.txt")]
;;only return lines that contain "awesome"
(doseq [line (filter #(re-find #"awesome" %) (line-seq fd))]
(js/console.log line))) ;;nothing but awesome

Boom. Pretty sweet.

Here’s a gist of the full code.

Find this post useful? Share the love!

Recent Posts