Copy File Line by Line Using lazy-seq

I’d like to copy a file line by line using a lazy-seq for buffering. The end goal is to filter/transform the lines, so while slurp/spit certainly would be easier, it doesn’t solve my bigger issue.

Here’s the code:

(defn copy-lines [source sink]
  (with-open [r (clojure.java.io/reader source)
              w (clojure.java.io/writer sink)
              lines (line-seq r)]
    (loop [l (first lines)
           ls (rest lines)]
      (when l
        (.write w (str l "\n"))
        (recur (first ls) (rest ls))))))

When I run (copy-lines "foo.txt" "bar.txt") on a non-empty foo.txt, I get the following error message:

Execution error (IllegalArgumentException) at scratch.core/copy-lines (core.clj:42).
No matching field found: close for class clojure.lang.Cons

At some point, the file descriptor must have been replaced by a lazy sequence made up of cons cells; at least that’s what I make out of the error message as a beginner.

Any hints what I’m doing/thinking wrong?

The problem is that you defined lines in with-open, which calls .close on it, and lines isn’t the type of thing you can .close.

You’ve got the right idea for not holding all of the source file in memory at once. Here’s a working version, in case that’s helpful:

user> (spit "foo.txt" "foobar")
nil
user> (slurp "foo.txt")
"foobar"
user> (defn copy-lines [src dest]
        (with-open [r (clojure.java.io/reader src)
                    w (clojure.java.io/writer dest)]
          (->> (line-seq r)
               (run! (fn [l] (.write w (str l "\n")))))))
#'user/copy-lines
user> (copy-lines "foo.txt" "bar.txt")
nil
user> (slurp "bar.txt")
"foobar\n"
1 Like

Thank you, @Harold, that makes totally sense to me now! The problem wasn’t the actual processing, but abusing with-open as a general let.