В чем разница между читательской монадой и частичной функцией в Clojure?
Леонардо Борхес подготовил фантастическую презентацию " Монады в Clojure". В ней он описывает читательскую монаду в Clojure, используя следующий код:
;; 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?
2 ответа
Монада читателей - это набор правил, которые мы можем применять для чистого сочинения читателей. Вы могли бы использовать 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)))
Обратите внимание, что вы должны явно переслать окружение этим читателям. Полный облом, верно? Используя правила, предоставляемые монадой читателя, мы можем получить гораздо более чистую композицию:
(def super-reader
(do-m reader-m
[x doubler
y plus-oner]
(+ x y)))
Вы можете использовать partial
"сделать" читателя монадой. Очередь let
в do-reader
делая синтаксическое преобразование на let
с partial
применение среды на правой стороне.
(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
является монадой личности (отношения обсуждаются здесь).
Действительно, поскольку в ответе Беямора на монаду читателя в вопросе Clojure использовалось только приложение "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 вы действительно можете получить функциональность do-нотации читателя монады, не вводя собственно монады. Аналогично выполнению преобразования ReaderT на монаде тождества, мы можем выполнить синтаксическое преобразование на let
, Как вы и предполагали, один из способов сделать это - частичное применение среды.
Возможно, больше Clojurish будет определять reader->
а также reader->>
синтаксически вставить среду как второй и последний аргумент соответственно. Я пока оставлю это в качестве упражнения для читателя.
Одним из выводов этого является то, что, хотя типы и классы типов в Haskell имеют много преимуществ, а структура монады - полезная идея, отсутствие ограничений системы типов в Clojure позволяет нам обрабатывать данные и программы в одном и том же способ и сделать произвольные преобразования в наших программах для реализации синтаксиса и управления, как мы считаем нужным.