如何编写 Clojure 线程宏?

jim*_*337 2 error-handling clojure

我正在尝试使用failjure/ok->>https://github.com/adambard/failjure#ok--and-ok-)编写线程宏,线程中的最后一个函数需要一个条件来执行。代码如下所示:

(f/ok->> (function1 param)
         (function2 param1 param 2)
         ...
         ({conditional function here}))

Run Code Online (Sandbox Code Playgroud)

如果条件未命中,线程宏返回倒数第二个函数调用的结果。我试图写一个cond是检查的必要条件,然后或者如果条件过去了,还是以前的功能而造成的返回的功能函数,但是线程宏似乎结果不传递给内部的功能cond,但只的cond本身。(不正确的)代码如下所示:

(f/ok->> (function1 param)
         (function2 param1 param 2)
         ...
         (cond (condition?)
             (function_if_passes_condition)
             #(%))
Run Code Online (Sandbox Code Playgroud)

我想知道是否有一种干净的方法可以正确地做到这一点。我想有可能编写一个具有这种功能的全新线程宏,但到目前为止我所有的尝试都没有奏效(我之前没有写过一个defmacro实现线程宏的程序,这非常困难)我是 clojure 的新手,有 3 个月的经验)。

Ala*_*son 7

你的问题陈述似乎有点模糊,所以我会解决这个问题的简化版本。

请记住,宏是一种代码转换机制。也就是说,它将您希望编写的代码转换为编译器可接受的代码。这样,最好将结果视为编译器扩展。编写宏很复杂,而且几乎总是不必要的。所以,除非你真的需要它,否则不要这样做。

让我们编写一个辅助谓词和单元测试:

(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)))
Run Code Online (Sandbox Code Playgroud)

假设我们要翻译这段代码:

  (check-> 10
    (+ 3)
    (* 100)
    (century?) )
Run Code Online (Sandbox Code Playgroud)

进入这个:

  (-> 10
    (+ 3)
    (* 100)
    (if (century) ; <= arg goes here
        :pass
        :fail))
Run Code Online (Sandbox Code Playgroud)

稍微重写一下目标:

  (let [x (-> 10    ; add a temp variable `x`
            (+ 3)
            (* 100))]
    (if (century? x) ; <= use it here
      :pass
      :fail))
Run Code Online (Sandbox Code Playgroud)

现在开始-impl功能。写一点,加上一些打印语句。仔细注意要使用的模式:

(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
Run Code Online (Sandbox Code Playgroud)

并通过单元测试驱动它。请务必遵循将 args 包装在带引号的 vector 中的模式。这模拟[& args]defmacro表达式中的作用。

(dotest
  (pprint/pprint
    (check->-impl '[10
                    (+ 3)
                    (* 100)
                    (century?)])
    ))
Run Code Online (Sandbox Code Playgroud)

结果:

args => [10 (+ 3) (* 100) (century?)]   ; 1 (from spyx)
[10 (+ 3) (* 100) (century?)]           ; 2 (from pprint)
Run Code Online (Sandbox Code Playgroud)

所以我们看到(1)中打印的结果,那么 impl 函数返回(2)中的(未修改的)代码。这是关键。宏返回修改后的代码。然后编译器编译修改后的代码代替原始代码。

用更多的打印写一些更多的代码:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args) ]
    (spyx all-but-last)        ; (1)
    (spyx last-arg)            ; (2)
))
Run Code Online (Sandbox Code Playgroud)

结果

all-but-last => (10 (+ 3) (* 100))    ; from (1)
last-arg     => (century?)            ; from (2)
(century?)                            ; from pprint
Run Code Online (Sandbox Code Playgroud)

注意发生了什么。我们看到了修改后的变量,但输出也发生了变化。再写一些代码:

(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
Run Code Online (Sandbox Code Playgroud)

哎呀!该append函数始终返回一个向量。只需用于->list将其转换为列表。你也可以输入(apply list ...).

cond-expr => (century? x)  ; better
Run Code Online (Sandbox Code Playgroud)

现在我们可以使用语法引用来创建我们的输出模板代码:

(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))))
Run Code Online (Sandbox Code Playgroud)

结果:

(clojure.core/let
 [tst.demo.core/x (clojure.core/-> 10 (+ 3) (* 100))]
 (if (century? x) :pass :fail))
Run Code Online (Sandbox Code Playgroud)

看到tst.demo.core/x部分了吗?这是一个问题。我们需要重写:

(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))))
Run Code Online (Sandbox Code Playgroud)

注意:正确使用~(eval) 和~@(eval-splicing)很重要。容易出错。现在我们得到

(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))
Run Code Online (Sandbox Code Playgroud)

试试看是真的。从引用的向量中解包 args,并调用宏而不是 impl 函数:

  (spyx-pretty :final-result
    (check-> 10
      (+ 3)
      (* 100)
      (century?)))
Run Code Online (Sandbox Code Playgroud)

带输出:

 :final-result
(check-> 10 (+ 3) (* 100) (century?)) => 
:pass
Run Code Online (Sandbox Code Playgroud)

并编写一些单元测试:

(dotest
  (is= :pass (check-> 10
               (+ 3)
               (* 100)
               (century?)))
  (is= :fail (check-> 10
               (+ 3)
               (* 101)
               (century?))))
Run Code Online (Sandbox Code Playgroud)

结果:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core

Ran 3 tests containing 4 assertions.
0 failures, 0 errors.
Run Code Online (Sandbox Code Playgroud)

您可能还对本书感兴趣: 掌握 Clojure 宏

在此处输入图片说明