Code minimizer - Remove unnecessary whitespace I often do golf challenges, and thought it might be advantageous to write some code that automatically removes all the unnecessary whitespace from a program.

After a few refactors, this is what I ended up with. In all the tests I've run it through, it's correctly removed only the whitespace that doesn't change the meaning of the program.

Example:

(minimize-code (fn [a] (+ a 1)))

"(fn[a](+ a 1))"


Limitations:


It doesn't handle line-comments well. It doesn't remove comments, so anything after the first ; is commented out.
It only removes unnecessary whitespace. Names still need to be manually shortened (maybe a later project).


What I what advice on:


Really, I'm looking for any advice. It actually ended up being much simpler than I anticipated, and I'm fairly happy with the end result. I'm sure there's room for improvement though!




(ns bits.golf.helpers.minimizer)

(def safe-neighbors
  "The characters that indicate neighboring spaces are safe to remove."
  (set "(){}[]\" \t\n"))

(def minimizer-output-path "./minimized-code.clj")

(defn can-remove-cur-char?
  "Returns whether or not a character is safe to remove.
   A character is considered unsafe to remove if it doesn't change the meaning of the program.
   Expects that if prev-char or next-char aren't available, nil will be passed."
  [prev-char cur-char next-char]
  (cond
    (not (Character/isWhitespace ^Character cur-char))
    false

    (or (nil? prev-char) (nil? next-char))
    true

    :else
    (or (safe-neighbors prev-char) (safe-neighbors next-char))))

(defn safe-remove-whitespace
  "Removes any whitespace characters that don't effect the meaning of the program."
  [code-str]
  (loop [[chr &amp; rest-str] code-str
         acc ""]
    (if-not chr
      acc
      (let [prev-chr (if (empty? acc) nil (last acc))
            next-chr (if rest-str (first rest-str) nil)
            can-remove? (can-remove-cur-char? prev-chr chr next-chr)
            replacement-chr (if (Character/isWhitespace ^Character chr)
                              \space chr)]
        (recur rest-str
               (if can-remove? acc (str acc replacement-chr)))))))

(defn minimize-code*
  "Removes unnecessary whitespace and outputs it to minimizer-output-path, then returns the minimized code."
  [code-str]
  (let [minimized-code (safe-remove-whitespace code-str)]
    (spit minimizer-output-path minimized-code)
    minimized-code))

(defmacro minimize-code
  "Removes unneccesary whitespace and outputs it to minimizer-output-path, then returns the minimized code."
  [&amp; body]
  (minimize-code* (apply str body)))
Here's something that gets the same answer using different techniques:

(def safe-neighbors
  (set "(){}[]\" \t\n"))

(defn whitesp? [ch]
  (Character/isWhitespace ^Character ch))

(defn del-white [s]
  (reduce #(if (and (whitesp? %2)
                    (whitesp? (last %1)))
             %1
             (str %1 %2)) "" s))

(defn squeezable? [[a b c]]
  (and (whitesp? b) (or (safe-neighbors a) (safe-neighbors c))))

(defn elide [triples]
  (let [squeezabilities (drop-last (cons true (map (complement squeezable?) triples)))]
    (-&gt;&gt; (map vector squeezabilities triples)
         (filter first)
         (map (comp first second)))))

(defn squeeze-safe [s]
  (apply str (apply str (-&gt;&gt; s (partition 3 1) elide)) (take-last 2 s)))

(defn minimize-code-2* [code-str]
  (-&gt;&gt; code-str
       del-white
       squeeze-safe))

(defmacro minimize-code [&amp; body]
  (minimize-code-2* (apply str body)))

(defn x-1 []
  (minimize-code (fn [a]    (+ a   1))))


In particular it removes recursion from being the crux of the code. Loop/recur recursion can solve most problems, but can quickly become liable to frailty and difficult to reason about as more and more loop variables are added. 

Here the starting off point was to think that partition can be used to look at the neighbouring characters by only grabbing three characters at a time. Thus partition can replace recursion. You know exactly what is going on with partition, however recursion is less focused, takes more time to comprehend and is prone to atrophy.

Here removing white-space and seeing if the final squeeze(*) can be done are two different operations, done on different parses through the input. Thus when writing (and reading) the code you can concentrate on one simple thing at a time.

Here squeezable? can be replaced by can-remove-cur-char?, as the latter is almost certainly more well thought out than my effort!

(*) Squeeze as in squeeze out the unnecessary white-space between neighbouring characters