嵌套 vs 线程 vs let



Clojure新人在这里,你认为以下哪种形式最"clojuresque":

  1. 重度嵌套

    (my-fn3 (my-fn2 (my-fn1 foo bar) baz) qux)
    
  2. 使用让

    (let [result foo
    result (my-fn1 result bar)
    result (my-fn2 result baz)
    result (my-fn3 result qux)])
    
  3. 使用线程优先

    (-> foo
    (my-fn1 bar)
    (my-fn2 baz)
    (my-fn3 qux))
    

我根据情况使用所有 3 种技术。 目标是选择使代码最清晰的技术(而不是最少的字符!

技术 #2 在调试时特别方便 ,因为您可以轻松打印出中间值。 但是,我通常会给每个阶段一个不同的名称来澄清情况:

(let [x-1 foo
x-2 (my-fn1 x-1 bar)
x-3 (my-fn2 x-2 baz)
x-4 (my-fn3 x-3 qux)]
(println :x-1 x-1)
(println :x-2 x-2)
(println :x-3 x-3)
(println :x-4 x-4)
x-4) ; don't forget to return the final result!

更新

关于调试的主题,这就是我会这样做的方式。 首先,3个版本原始:

(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(defn fa [x y] (+ x y))
(defn fb [x y] (* x y))
(defn fc [x y] {:x x :y y})
(def tgt 2)
(defn stack-nest
[foo]
(fc
(fb
(fa foo 3)
3)
99))
(defn stack-thread
[foo]
(-> foo
(fa 3)
(fb 3)
(fc 99)))
(defn stack-let
[foo]
(let [a foo
b (fa a 3)
c (fb b 3)
d (fc c 99)]
d)) ; don't forget to return the final result!

您不需要发明自己的dbg函数,因为图珀洛库中已经有一些不错的选择。在这里,我们使用spyx(间谍显式)宏打印结果:

(dotest
(spyx (stack-nest tgt))
(spyx (stack-thread tgt))
(spyx (stack-let tgt)))
(stack-nest tgt)       => {:x 15, :y 99}
(stack-thread tgt)     => {:x 15, :y 99}
(stack-let tgt)        => {:x 15, :y 99}

然后,我们使用spy和标签添加调试信息:

(defn stack-nest
[foo]
(spy :fc (fc
(spy :fb (fb
(spy :fa (fa foo 3))
3))
99)))
:fa => 5
:fb => 15
:fc => {:x 15, :y 99}
(stack-nest tgt) => {:x 15, :y 99}

它有效,但它非常丑陋。 螺纹形式怎么样? 在这里我们可以插入间谍,它更好一点:

(defn stack-thread
[foo]
(-> foo
(spy :foo)
(fa 3)
(spy :fa)
(fb 3)
(spy :fb)
(fc 99)
(spy :fc)
))
:foo => 2
:fa => 5
:fb => 15
:fc => {:x 15, :y 99}
(stack-thread tgt) => {:x 15, :y 99}

我们得到了我们想要的,但它有一些重复。 此外,我们需要将每个(spy ...)表达式放在单独的行上,以便线程宏->将值发送到计算(如(fa 3))和打印步骤(如(spy :fa))。

我们可以像这样使用it->宏来简化它:

(defn stack-thread-it
[foo]
(it-> foo
(fa it 3)
(fb it 3)
(fc 99 it)))

我们使用符号it作为占位符。 请注意,我们可以将线程值放置在任何参数位置,如反向参数所示fc. 对于我们的调试,请使用spyx使表达式自标记,我们得到:

(defn stack-thread-it
[foo]
(it-> (spyx foo)
(spyx (fa it 3))
(spyx (fb it 3))
(spyx (fc 99 it))))
foo         => 2
(fa it 3)   => 5
(fb it 3)   => 15
(fc 99 it)  => {:x 99, :y 15}
(stack-thread-it tgt) => {:x 99, :y 15}

当中间变量在let表达式中时,我像这样调试:

(defn stack-let
[foo]
(let [a  foo
>> (spyx a)
b  (fa a 3)
>> (spyx b)
c  (fb b 3)
>> (spyx c) ]
(spyx (fc c 99))))
a => 2
b => 5
c => 15
(fc c 99) => {:x 15, :y 99}
(stack-let tgt) => {:x 15, :y 99}

请注意,最后一个函数fc直接作为返回值调用(在let之外),但我们仍然可以使用spyx打印其值。

请注意,我喜欢使用符号>>(在 Clojure 中的任何地方未使用)而不是下划线_作为表达式值的虚拟接收者,因为有时很难看到下划线。>>符号不仅在代码中很突出,而且看起来有点像命令行提示符,提醒打印操作的命令性副作用性质。

我想说的是,通常大多数人在编写 Clojure 时都会努力将方法 2 和 3 结合起来,仅仅是因为它提供了最大的可读性。

通常,您需要使用 let,其中要绑定的值将在多个位置使用,例如,您需要一个用于创建另一个绑定的特定值,并且该值也将在 let 的主体中使用。

现在,方法 3 并不总是可以实现的,在某些情况下,您会遇到两个函数调用不共享您正在线程的值的相同序号位置的情况,这要么需要重新思考/构建代码略有不同,要么您可以使用 as-> 运算符,我个人认为这相当可怕。

我经常在 Clojure 中发现代码的可读性反映了它的惯用语,如果某些东西看起来正确,几乎总是有更好的方法来编写它。

最新更新