First, what are constant pairs? The term "constant pair" refers to a capability of zprint where, when formatting a list, it will search backwards from end end of a list, looking at every second element from the last (i.e., second from last, fourth from last, etc.) to see if it is a "constant". If it is, then all of the contiguous elements that have constants in those positions are formatted as "pairs".
Some examples would be helpful.
First an example from code using Seesaw, a Clojure library for building user interfaces using Java Swing:
(zprint dialogf {:parse-string? true :list {:constant-pair? false}})
; This is what you get with normal formatting, without constant pairs
(dialog
:parent
top-frame
:title
"Loading and Saving Data"
:content
(mig-panel
:items
[["To load data from a file, press:"]
[(action :name
"Load"
:handler
(fn [x]
(choose-file :type
:open
:success-fn
load-from-file
:dir
(clojure.java.io/file cwd)))) "wrap"]
[same-box "wrap"]
[(label :text
"To save data as it is entered, press:"
:id
:save-label
:class
:save)]
[(button :class
:save
:id
:save-button
:action
(action :name
"Save"
:handler
(fn [x]
(choose-file :type
:save
:success-fn
save-to-file
:dir
(clojure.java.io/file cwd))))) "wrap"]])
:visible?
false)
; This is what you get with the default zprint formatting, which recognizes
; constant pairs. Quite a difference!
(zprint dialogf {:parse-string? true})
(dialog :parent top-frame
:title "Loading and Saving Data"
:content
(mig-panel
:items
[["To load data from a file, press:"]
[(action :name "Load"
:handler (fn [x]
(choose-file :type :open
:success-fn load-from-file
:dir (clojure.java.io/file cwd))))
"wrap"] [same-box "wrap"]
[(label :text "To save data as it is entered, press:"
:id :save-label
:class :save)]
[(button :class :save
:id :save-button
:action (action :name "Save"
:handler (fn [x]
(choose-file
:type :save
:success-fn save-to-file
:dir (clojure.java.io/file
cwd))))) "wrap"]])
:visible? false)
Constant pairing also makes Clojure specs much more legible:
; First, without constant pairs, it looks like normal lisp formatting,
; which isn't great in this situation
(zprint spec {:parse-string? true :list {:constant-pair? false}})
(s/def ::line-seq
(s/nilable (s/coll-of (s/or :number
number?
:range
(s/coll-of number? :kind sequential?))
:kind
sequential?)))
; Now, with constant pair recognition (the default),it looks much better:
(zprint spec {:parse-string? true})
(s/def ::line-seq
(s/nilable (s/coll-of (s/or :number number?
:range (s/coll-of number? :kind sequential?))
:kind sequential?)))
Constant pairs are only recognized in lists. There are two basic controls for constant pairs:
-
{:list {:constant-pair? true}}
will enable (or disable them) altogether. -
{:list {:constant-pair-min 4}}
is the minimum number of non-whitespace elements required to exist before constant-pairs at the end of a list are rendered differently.
You can also control what is considered a "constant" for constant pairing.
By default, a "constant" is keyword, string, number, or true or false.
You can define a :constant-pair-fn
which, when given an element, will return
non-nil or nil to control whether or not that element is considered a
"constant".
Solely for the purposes of illustration (i.e., this is not how the
default form of constant pairing is implemented), here is a :constant-pair-fn
which mimics the default behavior:
{:list {:constant-pair-fn
#(or (keyword %) (string %) (number? %) (= true %) (= false %))}}
You can define such a function yourself, in order to allow additional items to
be treated as pairs and be controlled by the constant pairing options in
:list
.
The function takes one argument:
{:list {:constant-pair-fn (fn [element] ...)}}
See this discussion for more information
on user-defined functions and using sci
.
Here is an example of where this might be useful, also note the use of
:next-inner
to restrict the use of :constant-pair-fn
to just the
top level of m/app
:
(def mapp6
"(m/app :get (m/app middle1 middle2 middle3\n [route] handler\n\t\t ; How do comments work?\n [route] \n (handler this is \"a\" test \"this\" is \"only a\" test) \n\t\t )\n ; How do comments work here?\n :post (m/app \n [route] handler\n [route] ; What about comments here?\n\t\t handler))")
; Let's see what happens if we just use the default configuration.
; The narrow width is to force constant pairing on the second handler
; of the :get
(czprint mapp6 {:parse-string? true :width 55})
(m/app :get (m/app middle1
middle2
middle3
[route]
handler
; How do comments work?
[route]
(handler this
is
"a" test
"this" is
"only a" test))
; How do comments work here?
:post (m/app [route]
handler
[route] ; What about comments here?
handler))
; This is ok, but it would be nice to pair the handlers up with the routes
; Since they fall at the end of the expressions, sounds like we could use
; constant-pairing to force the pair behavior.
; Let's see what we can do if we define our own function to determine
; what constant-pairing will consdier a "constant"
(zprint
mapp6
{:parse-string? true,
:fn-map {"app" [:none
{:list {:constant-pair-min 1,
:constant-pair-fn #(or (vector? %) (keyword? %))},
:next-inner {:list {:constant-pair-fn nil,
:constant-pair-min 4}}}]},
:width 55})
(m/app :get (m/app middle1
middle2
middle3
[route] handler
; How do comments work?
[route] (handler this
is
"a" test
"this" is
"only a" test))
; How do comments work here?
:post (m/app [route] handler
[route] ; What about comments here?
handler))
; Much nicer. Note that we had to define both keywords and vectors as
; "constants", to preserve the keyword constant-pairing.
; Note also the use of :next-inner to restore constant-pairing to its
; default behavior down inside of expressions contained in `m/app`.
There is another (and better) approach to restoring the constant pairing
configuation inside of the expressions contained in m/app
- use
:next-inner-restore
. This integrates
better with any existing configuration for constant pairing in that it
doesn't reset it to a specific value for the contained expressions. Instead
it restores the previous values, whatever they were.
To illustrate this, let's see what happens if we don't restore the constant pairing configuration:
(zprint
mapp6
{:parse-string? true,
:fn-map {"app" [:none
{:list {:constant-pair-min 1,
:constant-pair-fn #(or (vector? %) (keyword? %))}}]}
:width 55})
(m/app :get (m/app middle1
middle2
middle3
[route] handler
; How do comments work?
[route] (handler this
is
"a"
test
"this"
is
"only a"
test))
; How do comments work here?
:post (m/app [route] handler
[route] ; What about comments here?
handler))
Note how the (handler this is "a" test "this" is "only a" test)
doesn't
pair up the constants with the things following them.
Now, when we restore the constant pairing configuration, we see that
constant pairing works as usual in the expressions contained in m/app
:
(zprint
mapp6
{:parse-string? true,
:fn-map {"app" [:none
{:list {:constant-pair-min 1,
:constant-pair-fn #(or (vector? %) (keyword? %))},
:next-inner-restore [[:list :constant-pair-min]
[:list :constant-pair-fn]]}]},
:width 55})
(m/app :get (m/app middle1
middle2
middle3
[route] handler
; How do comments work?
[route] (handler this
is
"a" test
"this" is
"only a" test))
; How do comments work here?
:post (m/app [route] handler
[route] ; What about comments here?
handler))
Using :next-inner-restore
allows any existing
constant pairing configuration to return for the inner expressions of
m/app
.
Typically, the user-defined function are specified in an options
map (often defining a new :style
) which appears in a .zprintrc
file. When .zprintrc
files are read-in, they are read using
sci
, the small Clojure interpreter. Thus, any user-defined
functions are defined using a large subset of the available Clojure
function, specifically excepting any functions that can be used to
operate outside of the sandbox provided by the sci
interpreter.
See this discussion for more information
on user-defined functions and using sci
.