Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.3k views
in Technique[技术] by (71.8m points)

error handling - How do I write a Clojure threading macro?

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).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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

enter image description here


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...