Last active
November 2, 2020 23:49
-
-
Save KingCode/773560f4ab5bf91e660a2a26e581b036 to your computer and use it in GitHub Desktop.
cond-let and cond-let> macros, to leverage bindings between test and result expressions, as well as earlier ones (for cond-let)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cond-let) | |
;; (cond-let | |
;; (odd? x) [x n] (inc x) | |
;; (< n 10) [y (inc n)] 10 | |
;; :else n)) | |
;; we want the above to yield | |
;; (let [x n] | |
;; (if (odd? x) | |
;; (inc x) | |
;; (let [y (inc n)] | |
;; (if (< n 10) | |
;; 10 | |
;; (if :else | |
;; n | |
;; (throw ...."no matching clause")))))) | |
(defmacro cond-let | |
"Takes ternary clauses which can use bindings visible to both the test | |
and result expression, as well as all following clauses (except when shadowed); | |
the last clause which can be binary and follows 'cond semantics. | |
Each ternary clause can be of the form | |
text-expr binding-vector result-expr | |
or | |
test-expr :>> result-expr | |
if there are no new bindings added to the clause | |
(:>> is an ordinary keyword) | |
" | |
[& clauses] | |
(let [emit (fn emit [args] | |
(let [[[pred binds expr :as clause] more] | |
(split-at 3 args) | |
n (count args)] | |
(cond | |
(= n 0) `(throw (IllegalArgumentException. | |
(str "No matching clause: " ~expr))) | |
(< n 2) `(throw (IllegalArgumentException. | |
(str "Must have at least 2 arguments: " | |
~@clause))) | |
(= n 2) | |
`(if ~pred | |
~(second clause) | |
~(emit more)) | |
(= :>> (second clause)) | |
`(if ~pred | |
~expr | |
~(emit more)) | |
:else | |
`(let ~binds | |
(if ~pred | |
~expr | |
~(emit more))))))] | |
(emit clauses))) | |
;; (cond-let> | |
;; (odd? x) [x n] (inc x) | |
;; (even? n) :>> (dec n) | |
;; (< 10 (+ y z)) [y (inc n) z 80] (* 2 n z) | |
;; :else n | |
;; we want the above to yield: | |
;; (or (let [x n] | |
;; (when (odd? x) | |
;; (inc x)) | |
;; (when (even? n) | |
;; (dec n)))) | |
;; (let [y (inc n) z 80] | |
;; (when (< 10 (+ y z)) | |
;; (* 2 n z))) | |
;; (when :else | |
;; n) | |
;; (throw..."No matching clause..")) | |
(defmacro cond-let> | |
"Same as for cond-let, except bindings are local to each clause only." | |
[& clauses ] | |
(let [emit (fn emit [args] | |
(let [[[pred binds expr :as clause] more] | |
(split-at 3 args) | |
n (count args)] | |
(cond | |
(= n 0) [`(throw (IllegalArgumentException. | |
(str "No matching clause: " ~expr)))] | |
(< n 2) [`(throw (IllegalArgumentException. | |
(str "Must have at least 2 arguments, only got: " | |
~@clause)))] | |
(= n 2) | |
(cons `(when ~pred | |
~(second clause)) | |
(emit more)) | |
(= :>> (second clause)) | |
(cons `(when ~pred | |
~(last clause)) | |
(emit more)) | |
:else | |
(cons `(let ~binds | |
(when ~pred | |
~expr)) | |
(emit more)))))] | |
`(or ~@(emit clauses)))) | |
(defn cond-let-sample [n] | |
(cond-let | |
(neg? x) [x n] (inc x) | |
(even? n) :>> (* (quot x n) (dec n)) | |
(< 9 (+ y z)) [y (inc n) z 3] (* 2 n z) | |
(= 7 (+ y z)) :>> "reused binding from previous clause" | |
:else n)) | |
(cond-let-sample -3) ;; => -2 | |
(cond-let-sample 34) ;; => 33 | |
(cond-let-sample 7) ;; => 42 | |
(cond-let-sample 3) ;; => 3 | |
(cond-let-sample 3) ;; =? "reused binding from previous clause" | |
(defn cond-let>-sample [n] | |
(cond-let> | |
(neg? x) [x n] (inc x) | |
(even? n) :>> (dec n) | |
(< 10 (+ y z)) [y (inc n) z 3] (* 2 n z) | |
:else n) | |
) | |
(cond-let>-sample -3) ;; => -2 | |
(cond-let>-sample 34) ;; => 33 | |
(cond-let>-sample 7) ;; => 42 | |
(cond-let>-sample 3) ;; => 3 | |
;; cond-let> clauses don't nest bindings, uncomment to try: | |
#_(cond-let> | |
(odd? x) [x 2] x | |
(even? x) :>> "even steven" ;; can't reuse higher up bindings! | |
:else :whatever) | |
;; => ...Unable to resolve symbol: x in this context |
Changes (November 2 2020):
- Bug-fix: for both macros, the throw clause for insufficient arguments was itself throwing because of evaluating the input clause as a list when reporting.
- The implementation of
cond-let>
now uses locallet
forms instead of inline functions, at @miikka 's suggestion, which is more aligned with the name of the macro. (This is transparent to the user). This takes actually less code than the inline version!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have come across a few situations where having this macro is worthwhile, specifically in cases where common values are sprinkled in both tests and outcome of various branches. Often, the following situations occur around the usage of
cond
:a value is part of either the test or the outcome of a branch, and occurs in more than one branch (most often)
inside a branch, both the test and outcome forms use a common value, or common logic.
infinite possible ways combining both of the above.
A common current solution is to create an enclosing
let
or other binding form creating all such values needed by all branches. It would be nice to have the option of an "as needed", more local way to bind values for re-use inside one or more branches. If a value is costly to compute, we have better performance in addition to improved readability.Hence a variant of the familiar
cond
macro which allows bindings at the branch level, in two variants (propagating downward, and strictly branch-local).Implementation:
Regarding the use of inline functions to implement the local-only bindings
cond-let>
version, it may be better to generate 'let bindings instead, as per the macro name. My vague motivation at the time was thatgenerating functions and wrapping them in a call seems easier than multiple lets whereas in cond-let the implementation is easier (a single 'let is generated).
for branches where no bindings are provided, a no-args function is more intuitive and purposeful than an empty 'let binding, although that is an implementation detail.
Usage
Regarding the user syntax, I like having the bindings in second place between the predicate and clause because it brings out the original
cond
syntax we're all used to. So this codecould be read as: "this predicate invoked with x being true, where x has the value expr, use x to produce this outcome/value", and so forth.
The name?
Finally, I am not even sure about the name for these two macros, which I think should be viewed as a kind of duality, or pair: maybe one of the following? For example:
cond-let
vscond-let>
(current)cond-let+
vscond-let
cond-let-all
vscond-let
cond-let*
vscond-let
I don't know how best to bring this functionality to be usable for most, so am grateful for comments, criticism and corrections/forks...Thanks!