2

Leonardo Borges对 Clojure 中的Monads进行了精彩的演示在其中,他使用以下代码描述了 Clojure 中的 reader monad :

;; Reader Monad

(def reader-m
  {:return (fn [a]
             (fn [_] a))
   :bind (fn [m k]
           (fn [r]
             ((k (m r)) r)))})

(defn ask  []  identity)
(defn asks [f]
  (fn [env]
    (f env)))

(defn connect-to-db []
  (do-m reader-m
        [db-uri (asks :db-uri)]
        (prn (format "Connected to db at %s" db-uri))))

(defn connect-to-api []
  (do-m reader-m
        [api-key (asks :api-key)
         env (ask)]
        (prn (format "Connected to api with key %s" api-key))))

(defn run-app []
  (do-m reader-m
        [_ (connect-to-db)
         _ (connect-to-api)]
        (prn "Done.")))

((run-app) {:db-uri "user:passwd@host/dbname" :api-key "AF167"})
;; "Connected to db at user:passwd@host/dbname"
;; "Connected to api with key AF167"
;; "Done."

这样做的好处是您正在以纯粹的功能方式从环境中读取值。

但是这种方法看起来非常类似于 Clojure 中的偏函数。考虑以下代码:

user=> (def hundred-times (partial * 100))
#'user/hundred-times

user=> (hundred-times 5)
500

user=> (hundred-times 4 5 6)
12000

我的问题是:读者单子和 Clojure 中的部分函数有什么区别?

4

2 回答 2

4

reader monad 是一组规则,我们可以应用它来干净地编写 reader。你可以partial用来做一个阅读器,但它并没有真正给我们提供一种将它们放在一起的方法。

例如,假设您想要一个读取器的读取值翻倍。您可以使用partial它来定义它:

(def doubler
  (partial * 2))

您可能还希望阅读器在其读取的任何值上添加一个:

(def plus-oner
  (partial + 1))

现在,假设您想将这些人组合在一个阅读器中,以添加他们的结果。你可能会得到这样的结果:

(defn super-reader
  [env]
  (let [x (doubler env)
        y (plus-oner env)]
    (+ x y)))

请注意,您必须将环境显式转发给这些读者。完全无赖,对吧?使用 reader monad 提供的规则,我们可以获得更清晰的组合:

(def super-reader
  (do-m reader-m
    [x doubler
     y plus-oner]
    (+ x y)))
于 2014-03-08T05:09:30.300 回答
3

可以使用partial“做”阅读器单子。通过使用右侧环境的应用程序进行句法转换let变成a 。do-readerletpartial

(defmacro do-reader
  [bindings & body] 
  (let [env (gensym 'env_)
        partial-env (fn [f] (list `(partial ~f ~env)))
        bindings* (mapv #(%1 %2) (cycle [identity partial-env]) bindings)] 
    `(fn [~env] (let ~bindings* ~@body))))

然后do-reader是读者单子和let身份单子(这里讨论的关系)。

实际上,由于Beyamor 在 Clojure question 中对您的 reader monad 的回答中仅使用了 reader monad 的“do notation”应用程序,因此相同的示例将按上述m/domonad Reader替换do-reader为。

但是,为了多样化,我将修改第一个示例,使其在环境映射方面更加 Clojurish,并利用关键字可以充当函数的事实。

(def sample-bindings {:count 3, :one 1, :b 2})

(def ask identity)

(def calc-is-count-correct? 
  (do-reader [binding-count :count 
              bindings ask] 
    (= binding-count (count bindings))))

(calc-is-count-correct? sample-bindings)
;=> true

第二个例子

(defn local [modify reader] (comp reader modify))

(def calc-content-len 
  (do-reader [content ask] 
    (count content)))

(def calc-modified-content-len
  (local #(str "Prefix " %) calc-content-len))

(calc-content-len "12345")
;=> 5

(calc-modified-content-len "12345")
;=> 12

请注意,由于我们建立在 上let,我们仍然可以使用破坏。愚蠢的例子:

(def example1 
  (do-reader [a :foo
              b :bar] 
    (+ a b)))

 (example1 {:foo 2 :bar 40 :baz 800})
 ;=> 42

 (def example2 
   (do-reader [[a b] (juxt :foo :bar)]
     (+ a b)))

(example2 {:foo 2 :bar 40 :baz 800})
;=> 42

因此,在 Clojure 中,您确实可以在不适当引入 monad 的情况下获得 reader monad 的 do 表示法的功能。类似于对身份单子进行 ReaderT 转换,我们可以对let. 正如您所推测的,一种方法是部分应用环境。

也许更多 Clo​​jurish 会定义 areader->reader->>在语法上分别插入环境作为第二个和最后一个参数。我现在把这些作为练习留给读者。

从中得出的一个结论是,虽然 Haskell 中的类型和类型类有很多好处,而且 monad 结构是一个有用的想法,但没有 Clojure 中类型系统的约束,我们可以将数据和程序放在同一个地方方式并对我们的程序进行任意转换,以实现我们认为合适的语法和控制。

于 2014-03-09T21:33:17.803 回答