【问题标题】:Using let style destructuring for def对 def 使用 let 样式解构
【发布时间】:2017-07-15 18:31:02
【问题描述】:

是否有一种合理的方法可以让多个def 语句发生与let 相同的破坏方式?例如:

(let [[rtgs pcts] (->> (sort-by second row)
                       (apply map vector))]
 .....)

我想要的是这样的:

(defs [rtgs pcts] (->> (sort-by second row)
                       (apply map vector)))

这在 REPL、笔记本和调试时经常出现。真的感觉像是缺少一个功能,所以我想获得以下方面的指导

  • 这已经存在,我想念它
  • 这是个坏主意,因为...(变量捕获?,不习惯?,Rich 这么说的?)
  • 这只是不必要的,我一定是从一种邪恶的语言中退出。 (同:不要将我们的语言与您的宏混淆)

一个超短的实验给了我类似的东西:

(defmacro def2 [[name1 name2] form] 
  `(let [[ret1# ret2#] ~form]
      (do (def ~name1 ret1#)
      (def ~name2 ret2#))))

它的工作原理如下:

(def2 [three five] ((juxt dec inc) 4))
three ;; => 3
five ;; => 5

当然,该宏的“工业实力”版本可能是:

  • 检查名称的数量是否与输入的数量相匹配。 (从表单返回)
  • 递归调用以处理更多名称(我可以在这样的宏中执行此操作吗?)

【问题讨论】:

  • 看起来这个功能可能不存在,但我认为它肯定会非常有用。我在其他语言中大量使用了这种模式(虽然,也许不是为了定义全局或命名空间级别的符号?)。有时函数想要返回多个值,而有时您可能想要定义这些值,而无需太多麻烦。

标签: clojure macros


【解决方案1】:

虽然我同意 Josh 的观点,您可能不应该在生产环境中运行它,但我认为在 repl 中使用它作为一种便利并没有什么害处(事实上,我认为我会将它复制到我的调试中 - repl 厨房水槽库)。

我喜欢编写宏(尽管通常不需要它们),所以我编写了一个实现。它接受任何绑定形式,例如let

(我先写了这个规范,但是如果你使用的是 clojure

(ns macro-fun
  (:require
   [clojure.spec.alpha :as s]
   [clojure.core.specs.alpha :as core-specs]))

(s/fdef syms-in-binding
  :args (s/cat :b ::core-specs/binding-form)
  :ret (s/coll-of simple-symbol? :kind vector?))

(defn syms-in-binding
  "Returns a vector of all symbols in a binding form."
  [b]
  (letfn [(step [acc coll]
            (reduce (fn [acc x]
                      (cond (coll? x) (step acc x)
                            (symbol? x) (conj acc x)
                            :else acc))
                    acc, coll))]
    (if (symbol? b) [b] (step [] b))))

(s/fdef defs
  :args (s/cat :binding ::core-specs/binding-form, :body any?))

(defmacro defs
  "Like def, but can take a binding form instead of a symbol to
   destructure the results of the body.
   Doesn't support docstrings or other metadata."
  [binding body]
  `(let [~binding ~body]
     ~@(for [sym (syms-in-binding binding)]
         `(def ~sym ~sym))))


;; Usage

(defs {:keys [foo bar]} {:foo 42 :bar 36})

foo ;=> 42

bar ;=> 36

(defs [a b [c d]] [1 2 [3 4]])

[a b c d] ;=> [1 2 3 4]

(defs baz 42)

baz ;=> 42

关于您的 REPL 驱动开发评论:

我对 Ipython 没有任何经验,但我会简要解释一下我的 REPL 工作流程,您也许可以评论与 Ipython 的任何比较/对比。

我从来没有像终端一样使用我的 repl,输入命令并等待回复。我的编辑器支持(emacs,但任何 clojure 编辑器都应该这样做)将光标放在任何 s 表达式的末尾并将其发送到 repl,在光标之后“打印”结果。

我通常在我开始工作的文件中有一个comment 块,只需输入任何内容并对其进行评估。然后,当我对结果相当满意时,我将其从“repl-area”中拉出并进入“real-code”。

(ns stuff.core)

;; Real code is here.
;; I make sure that this part always basically works,
;; ie. doesn't blow up when I evaluate the whole file

(defn foo-fn [x]
  ,,,)

(comment

  ;; Random experiments.

  ;; I usually delete this when I'm done with a coding session,
  ;; but I copy some forms into tests.

  ;; Sometimes I leave it for posterity though,
  ;; if I think it explains something well.

  (def some-data [,,,])

  ;; Trying out foo-fn, maybe copy this into a test when I'm done.
  (foo-fn some-data)

  ;; Half-finished other stuff.
  (defn bar-fn [x] ,,,)

  (keys 42) ; I wonder what happens if...

  )

您可以在clojure core source code 中查看此示例。

【讨论】:

  • 回复:我的 REPL cmets。比库或后端开发人员考虑更多的“数据科学家”工作流程。我喜欢 clojure,并希望它得到更广泛的采用。 Emacs/cider repl 很好,非常好,但它是特定于 IDE 的(就像 proto-repl 等一样)Emacs/cider 的墙太陡了,对于学习该语言的人来说是难以攀登的。内置的 repl 有明显的失误,但要列出一些:1)没有多行编辑或历史记录,2)没有自动右大括号,3)更多地使用 pprint,4)文件名的制表符完成,5)能够执行 shell 基础知识,例如 pwd、ls 和编辑...(空间不足)
【解决方案2】:

任何 clojure 将拥有的defs 的数量会因项目而异,但我想说的是,一般来说,defs 并不经常是某些计算的结果,更不用说是需要解构的计算。 defs 更多时候是起点,之后的一些计算将依赖于这个值。

通常函数更适合计算值;如果计算成本很高,那么您可以记住该函数。如果你觉得你真的需要这个功能,那么一定要使用你的宏——这是 clojure 的卖点之一,即可扩展性!但总的来说,如果您觉得自己需要此构造,请考虑您过度依赖全局状态的可能性。

只是举一些真实的例子,我只是参考了我工作中的主要项目,大概是 2K-3K 行的 clojure,在大约 20 个命名空间中。我们有大约 20 个defs,其中大部分被标记为私有,其中没有一个实际上在计算任何东西。我们有这样的东西:

(def path-prefix "/some-path")
(def zk-conn (atom nil))
(def success? #{200})
(def compile* (clojure.core.memoize/ttl compiler {} ...)))
(def ^:private nashorn-factory (NashornScriptEngineFactory.))
(def ^:private read-json (comp json/read-str ... ))

定义函数(使用compmemoize)、枚举、通过atom 的状态——但没有真正的计算。

所以我想说,根据您上面的要点,这介于 2 和 3 之间:这绝对不是需要的常见用例(您是我听说过的第一个想要这个的人,所以无论如何,这对我来说并不常见);它不常见的原因是因为我上面所说的,即它可能是一种代码异味,表明依赖于过多的全局状态,因此不会很惯用。

我对我的大部分代码进行的一个试金石测试是:如果我将这个函数拉出这个命名空间并将其粘贴到另一个命名空间中,它仍然有效吗?移除对外部变量的依赖使得测试更容易,代码更模块化。有时我们需要它,因此请查看您的要求并相应地进行。祝你好运!

【讨论】:

  • 我总体上同意关于全球状态的警告,但这阻碍了我经常听到的“REPL”驱动的开发。它在 clojure 中“还可以”,但与我习惯的更成熟的 repl(如 ipython)相比就相形见绌了。
  • REPL 驱动的开发并不意味着您应该按原样获取代码(为方便起见,将临时数据分配给 vars),并将其放入源文件中,除非我误解了您的观点关于状态和RDD的关系。与 ipython 相比,它如何苍白? @PhilCooper
猜你喜欢
  • 1970-01-01
  • 2014-07-27
  • 2021-04-07
  • 2017-05-07
  • 1970-01-01
  • 2021-04-24
相关资源
最近更新 更多