我在写的一些单子中出现了意想不到的行为。我创建了一个 parser-m monad
(def parser-m (state-t maybe-m))
我使用 m-plus 作为一种贯穿查询机制,在我的例子中,它首先从缓存(数据库)中读取值,如果返回 nil,下一个方法是从“live”中读取( REST 调用)。
但是,总是调用 m-plus 列表中的第二个值,即使它的值被忽略(如果缓存命中良好)并且最终返回的是第一个单子函数的值。
这是我看到的问题的简化版本,以及我找到的一些解决方案,但我不知道为什么。
我的问题是:
- 这是预期的行为还是 m-plus 中的错误?即,即使第一项返回值,m-plus 列表中的第二个方法是否总是被评估?
- 与上述相比较小,但如果我
_ (fetch-state)
从检查器中删除调用,当我评估该方法时,它会打印出 m-plus 正在调用的函数的消息(当我认为它不应该调用时)。这也是bug吗?
这是有问题的代码的精简版本,突出了问题。它只是检查传入的键/值对是否与初始状态值相同,并更新状态以标记它实际运行的内容。
(ns monads.monad-test
(:require [clojure.algo.monads :refer :all]))
(def parser-m (state-t maybe-m))
(defn check-k-v [k v]
(println "calling with k,v:" k v)
(domonad parser-m
[kv (fetch-val k)
_ (do (println "k v kv (= kv v)" k v kv (= kv v)) (m-result 0))
:when (= kv v)
_ (do (println "passed") (m-result 0))
_ (update-val :ran #(conj % (str "[" k " = " v "]")))
]
[k v]))
(defn filler []
(println "filler called")
(domonad parser-m
[_ (fetch-state)
_ (do (println "filling") (m-result 0))
:when nil]
nil))
(def checker
(domonad parser-m
[_ (fetch-state)
result (m-plus
;; (filler) ;; intitially commented out deliberately
(check-k-v :a 1)
(check-k-v :b 2)
(check-k-v :c 3))]
result))
(checker {:a 1 :b 2 :c 3 :ran []})
当我按原样运行时,输出是:
> (checker {:a 1 :b 2 :c 3 :ran []})
calling with k,v: :a 1
calling with k,v: :b 2
calling with k,v: :c 3
k v kv (= kv v) :a 1 1 true
passed
k v kv (= kv v) :b 2 2 true
passed
[[:a 1] {:a 1, :b 2, :c 3, :ran ["[:a = 1]"]}]
我不希望这条线k v kv (= kv v) :b 2 2 true
出现。最终结果是从第一个函数返回给 m-plus 的值,正如我所料,但我不希望第二个函数被调用。
现在,我发现如果我将填充符传递给不执行任何操作的 m-plus(即取消注释该(filler)
行),那么输出是正确的,不会评估 :b 值。
如果我没有填充方法,并使第一个方法测试失败(即(check-k-v :a 2)
再次将其更改为 then 一切都很好,我没有接到检查 :c 的电话,只有 a 和 b 被测试。
根据我对state-t maybe-m
转换给我的理解,m-plus 函数应该如下所示:
(defn m-plus
[left right]
(fn [state]
(if-let [result (left state)]
result
(right state))))
这意味着right
除非left
返回 nil/false,否则不会调用它。
编辑: 在查看 state-t 和可能 -m 源之后,m-plus 看起来更像:
(fn [& statements]
(fn [state]
(apply (fn [& xs]
(first (drop-while nil? xs)))
(map #(% state) statements))))
但原理是一样的,(first (drop-while nil? ...)
只应在返回有效值的项目上执行。
我很想知道我的理解是否正确,以及为什么我必须使用填充方法来停止额外的评估(我不想发生这些影响)。
编辑:
如果我转而使用Jim Duey 的 parser-m 手写实现(来自他的优秀博客),则不会评估 m-plus 中的第二个函数,这似乎意味着转换 monad 正在破坏 m-plus。然而,即使在这个实现中,如果我删除函数中的初始(fetch-state)
调用checker
,domonad 定义会导致创建 m-plus 函数的输出,这表明在 domonad 的实现中会发生一些我没有预料到的事情。
为冗长的帖子道歉!