如何编写Clojure线程宏? [英] How do I write a Clojure threading macro?
问题描述
我正在尝试使用 failjure / ok->>
(
I am attempting to write a threading macro using failjure/ok->>
(https://github.com/adambard/failjure#ok--and-ok-), with the final function in the thread requiring a condition to execute. The code looks something like this:
(f/ok->> (function1 param)
(function2 param1 param 2)
...
({conditional function here}))
where if the conditional is not hit, the threading macro returns the result of the penultimate function call.
I attempted to write a cond
function that checked for the necessary condition and then either returned the function if the condition passed, or just the result of the previous function, but the threading macro seems to not pass the result to the function within the cond
, but only the cond
itself. the (incorrect) code looked like this:
(f/ok->> (function1 param)
(function2 param1 param 2)
...
(cond (condition?)
(function_if_passes_condition)
#(%))
I am wondering if there is a clean way to do this correctly. I imagine it is possible to write a brand new threading macro with such functionality, but so far all of my attempts at doing that haven't worked (I have not written a defmacro
that implements a threading macro before, and it has been quite difficult as I am fairly new at clojure with 3 months experience).
Your problem statement seems a little vague, so I'll solve a simplified version of the problem.
Keep in mind that a macro is a code-translation mechanism. That is, it translates the code that you wish you could write into something that is acceptable by the compiler. In this way, it is best to think of the result as a compiler extension. Writing a macro is complicated and almost always unnecessary. So, don't do it unless you really need it.
Let's write a helper predicate and unit test:
(ns tst.demo.core
(:use tupelo.core tupelo.test) ; <= *** convenience functions! ***
(:require [clojure.pprint :as pprint]))
(defn century? [x] (zero? (mod x 100)))
(dotest
(isnt (century? 1399))
(is (century? 1300)))
Suppose we want to translate this code:
(check-> 10
(+ 3)
(* 100)
(century?) )
into this:
(-> 10
(+ 3)
(* 100)
(if (century) ; <= arg goes here
:pass
:fail))
Re-write the goal a little:
(let [x (-> 10 ; add a temp variable `x`
(+ 3)
(* 100))]
(if (century? x) ; <= use it here
:pass
:fail))
Now start on the -impl
function. Write just a little, with some print statements. Notice carefully the pattern to use:
(defn check->-impl
[args] ; no `&`
(spyx args) ; <= will print variable name and value to output
))
(defmacro check->
[& args] ; notice `&`
(check->-impl args)) ; DO NOT use syntax-quote here
and drive it with a unit test. Be sure to follow the pattern of wrapping the args in a quoted vector. This simulates what [& args]
does in the defmacro
expression.
(dotest
(pprint/pprint
(check->-impl '[10
(+ 3)
(* 100)
(century?)])
))
with result:
args => [10 (+ 3) (* 100) (century?)] ; 1 (from spyx)
[10 (+ 3) (* 100) (century?)] ; 2 (from pprint)
So we see the result printed in (1), then the impl function returns the (unmodified) code in (2). This is key. The macro returns modified code. The compiler then compiles the modified code in place of the original.
Write some more code with more prints:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args) ]
(spyx all-but-last) ; (1)
(spyx last-arg) ; (2)
))
with result
all-but-last => (10 (+ 3) (* 100)) ; from (1)
last-arg => (century?) ; from (2)
(century?) ; from pprint
Notice what happened. We see our modified variables, but the output has changed as well. Write some more code:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args)
cond-expr (append last-arg 'x)] ; from tupelo.core
(spyx cond-expr)
))
cond-expr => [century? x] ; oops! need a list, not a vector
Oops! The append
function always returns a vector. Just use ->list
to convert it into a list. You could also type (apply list ...)
.
cond-expr => (century? x) ; better
Now we can use the syntax-quote to create our output template code:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args)
cond-expr (->list (append last-arg 'x))]
; template for output code
`(let [x (-> ~@all-but-last)] ; Note using `~@` eval-splicing
(if ~cond-expr
:pass
:fail))))
with result:
(clojure.core/let
[tst.demo.core/x (clojure.core/-> 10 (+ 3) (* 100))]
(if (century? x) :pass :fail))
See the tst.demo.core/x
part? That is a problem. We need to re-write:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args)]
; template for output code. Note all 'let' variables need a `#` suffix for gensym
`(let [x# (-> ~@all-but-last) ; re-use pre-existing threading macro
pred-result# (-> x# ~last-arg)] ; simplest way of getting x# into `last-arg`
(if pred-result#
:pass
:fail))))
NOTE: It is important to use ~
(eval) and ~@
(eval-splicing) correctly. Easy to get wrong. Now we get
(clojure.core/let
[x__20331__auto__ (clojure.core/-> 10 (+ 3) (* 100))
pred-result__20332__auto__ (clojure.core/-> x__20331__auto__ (century?))]
(if pred-expr__20333__auto__
:pass
:fail))
Try it out for real. Unwrap the args from the quoted vector, and call the macro instead of the impl function:
(spyx-pretty :final-result
(check-> 10
(+ 3)
(* 100)
(century?)))
with output:
:final-result
(check-> 10 (+ 3) (* 100) (century?)) =>
:pass
and write some unit tests:
(dotest
(is= :pass (check-> 10
(+ 3)
(* 100)
(century?)))
(is= :fail (check-> 10
(+ 3)
(* 101)
(century?))))
with result:
-------------------------------
Clojure 1.10.1 Java 13
-------------------------------
Testing tst.demo.core
Ran 3 tests containing 4 assertions.
0 failures, 0 errors.
You may also be interested in this book: Mastering Clojure Macros
这篇关于如何编写Clojure线程宏?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!