1 October 2021

'Partial' String Formatting in Clojure

The 'partial' function effectively creates constants from incomplete function calls!

I was recently doing a bunch of string formatting to produce a series of SVG Path commands that would draw an 'X' for a Tic-Tac-Toe game developed in ClojureScript.

(defn- make-x-mark [x y]
  (let [path (apply str (compose-x-path x y))]
    [:path {:class :mark-x, :d path}]))

The SVG path element allows you to specify a series of 'commands' to a 'pen' that can be moved to draw lines. Basically, it's a very flexible way to draw any shape. My goal was to define a single path element to draw the 'X' game symbol by doing the following (non-optimized steps):

  1. move the pen (without drawing) to what would be the center of the 'X'
  2. draw a line from the center to form the upper-left 'arm' of the 'X'
  3. move the pen (without drawing) back to the center (starting point)
  4. draw the upper-right 'arm'
  5. move the pen (without drawing) back to the center (starting point)
  6. draw the lower-left 'arm'
  7. move the pen (without drawing) back to the center (starting point)
  8. draw the lower-right 'arm'

The compose-x-path function referenced above receives the [x, y] center point and produces the sequence of 'move' (M) and 'line' (L) commands described above. Here's what we expect from that function (given an x value of 1 and a y value of 2):

(it "composes the path of the rendered 'X' mark"
  (should= ["M 1 2 ", "L 0.75 1.75 "  ; center -> upper-left
            "M 1 2 ", "L 1.25 1.75 "  ; center -> upper-right
            "M 1 2 ", "L 0.75 2.25 "  ; center -> lower-left
            "M 1 2 ", "L 1.25 2.25 "] ; center -> lower-right
           (compose-x-path 1 2)))

Here's an early draft of the compose-x-path function:

(defn compose-x-path [x y]
  (let [len            0.25
        center         (string/format "M %f %f" x y)
        to-upper-left  (string/format "L %f %f" (- x len) (- y len))
        to-upper-right (string/format "L %f %f" (+ x len) (- y len))
        to-lower-left  (string/format "L %f %f" (- x len) (+ y len))
        to-lower-right (string/format "L %f %f" (+ x len) (+ y len))]
    [center to-upper-left
     center to-upper-right
     center to-lower-left
     center to-lower-right]))

Aside: The familiar format function we normally use from clojure.core isn't available in ClojureScript except through the 'Google Closure Library' (goog.string.format). It is generally 'required' and then used as follows:

(ns rostering.components.services
  (:require
    [goog.string :as gstring]
    [goog.string.format]))

(string/format "%s" "Hello")

Ok, back to the compose-x-path function. Notice the duplication of the format strings and also how the string/format calls clutter up the essence of the function because they are at a slightly lower level of abstraction. This is where the partial function comes in very handy!

(def move (partial string/format "M %f %f "))
(def line (partial string/format "L %f %f "))

The partial function:

Takes a function f and fewer than the normal arguments to f, and returns a fn that takes a variable number of additional args. When called, the returned function calls f with args + additional args.

(Source: https://clojuredocs.org/clojure.core/partial)

So, it's like we are freezing a 'partial' call to a function for use later!

(defn compose-x-path [x y]
  (let [len            0.25
        center         (move x y)
        to-upper-left  (line (- x len) (- y len))
        to-upper-right (line (+ x len) (- y len))
        to-lower-left  (line (- x len) (+ y len))
        to-lower-right (line (+ x len) (+ y len))]
    [center to-upper-left
     center to-upper-right
     center to-lower-left
     center to-lower-right]))

Much better!

Except, you may have thought of a more efficient way to draw the same shape (as I just have). The line command I've been using draws from the where the 'pen' currently is to a specified point. What if we just drew two long lines that intersected to make the X? We can do that by using a 'line' command that receives both the starting and the stopping points.

(it "composes the path of the rendered 'X' mark"
  (should= ["M 0.75 1.75 "  ; move upper-left
            "L 1.25 2.25 "  ; line lower-right
            "M 1.25 1.75 "  ; move upper-right
            "L 0.75 2.25 "] ; line lower-left
           (compose-x-path 1 2)))

...

(def move (partial string/format "M %f %f "))
(def line (partial string/format "L %f %f "))

(defn compose-x-path [x y]
  (let [len 0.25]
    [(move (- x len) (- y len))    ; move upper-left
     (line (+ x len) (+ y len))    ; line lower-right
     (move (+ x len) (- y len))    ; move upper-right
     (line (- x len) (+ y len))])) ; line lower-left

Even better! Hey, thanks for being my 'rubber duck' on that one.