Konrad Hinsen,Jim Duey和Leonardo Borges在Clojure中有一些杰出的作品。
我的问题是 - 是否有可能在Clojure中做免费Monad?
来自Scala文章的This is an example in Haskell:
data Free f r = Free (f (Free f r)) | Pure r
这是相应的Scala示例
sealed abstract class Free[S[+_], +A](implicit S: Functor[S]) {
final def map[B](f: A => B): Free[S, B] =
flatMap(a => Return(f(a)))
final def flatMap[B](f: A => Free[S, B]): Free[S, B] = this match {
case Gosub(a, g) => Gosub(a, (x: Any) => Gosub(g(x), f))
case a => Gosub(a, f)
}
...
}
答案 0 :(得分:8)
绝对可以做到,但关键是自由monad的惯用Haskell实现基于利用类型系统来提供某种模块化和良好的形成保证。在Clojure中使用的成语可能不是惯用语,最好能够提出不同的实现方式。
看看Haskell中Free
monad的完整定义:
data Free f r = Free (f (Free f r)) | Pure r
instance Functor f => Monad (Free f) where
-- Construct a "pure value" leaf node
return = Pure
-- Replace every "pure value" leaf node with the tree obtained by
-- applying `f` to its value. (Remember that Haskell is lazy, so this
-- tree is only created as it is consumed, and only those branches that are
-- in fact consumed.)
Pure a >>= f = f a
Free fa >>= f = Free (fmap (>>=f) fa)
这是使用以下Haskell功能:
Functor
和Monad
Free
是递归类型f
中的类型变量Free f r
实际上用作类型构造函数 - 定义在容器类型上进行抽象,同时约束元素类型。然后它也使用了类型级别的定点构造,这是Clojure中不存在的一种技术。最简单的版本是这种类型:
newtype Fix f = Fix (f (Fix f))
如果你曾经读过关于Y combinator的内容,你可以把它看作是它的类比,但是在类型层面而不是价值层面。
但是抛开所有这些,让我们专注于这里的基本结构:一个免费的monad基本上是一种懒惰生成的,可能是无限的树结构。免费monad中使用的Functor
做了两件事:
(不要陷入将自由monadic树视为“抽象语法树”的错误观念;树的节点不代表表达式或类似的东西。)
核心通用免费monad代码然后提供两件事:
Pure
节点类型。这些节点只包含一个值,没有子节点。Pure
叶子,并将其值应用于函数。完成此操作后,通过提供合适的仿函数,您可以使用通用的免费monad代码根据仿函数提供的结构构建惰性树。这些树只是惰性值;您可以通过编写根据某种策略导航树的解释器函数来使用它们,以便生成所需的结果。但实际上,您希望您的免费monad库具有一些合适的通用实用程序功能,以帮助您更轻松地编写解释器。例如:
-- | Generic recursive fold over a free monadic tree.
iter :: Functor f => (f a -> a) -> Free f a -> a
iter f (Pure a) = a
iter f (Free fa) = f (fmap (iter f) fa)
-- | If you can map any node to a corresponding action in another monad, you can map
-- the whole free monadic tree to a monadic action as well...
interpret :: (Functor f, Monad m) => (forall x. f x -> m x) -> Free f a -> m a
所以,如果有人想在Clojure(或任何其他Lisp)中编写这个问题,那么显而易见的问题是:为什么要这样做而不只是编写一个s-expression DSL解释器呢?
嗯,免费monad给你的一件事是一种monadic程序的正常形式表示。例如,想一想Clojure和Haskell代码的以下类似片段:
;;; Clojure; these two expressions always produce the same result and
;;; have the same effects
(do a (do b c))
(do (do a b) c)
-- Haskell counterparts
(a >> b) >> c
a >> (b >> c)
Clojure表单中的s-expression语法允许您编写两个不同的表达式,但仍然需要这些表达式始终表现相同。另一方面,在Haskell中,Free
monad定义保证上面的两个表达式求值完全相同的值 -no程序可以区分它们!因此,您可以编写一个有缺陷的s-exp解释器或宏来对待这两个Clojure表单的方式不同,但对于免费的monadic树则不然。但
另一组潜在的优势是免费monad提供了一些标准技术来实现回溯等语言功能(使用某种列表作为你的函子,代表它们被考虑的顺序的替代品)和暂停/恢复程序的解释(免费monad与继续传递风格密切相关)。
但是对于Lisp中的最大惯用性,您可能希望结合使用这两种技术:根据客户提供的特殊定义,使用s-exprs作为前端,使用一些通用库转换为自由monad表示。 DSL的运作。还提供通用函数,以简化客户为其免费monadic语言编写解释器的任务。
答案 1 :(得分:4)
是的,按照路易斯·卡西利亚斯的回答,这是一个在clojure中隐藏Free
monad的实现。
(use 'clojure.algo.monads)
;; data Free f r = Free (f (Free f r)) | Pure r
(defn pure [v] {:t :pure :v v})
(defn impure [v] {:t :impure :v v })
(defn free-monad
[fmap]
(letfn [
(fm-result [v] (pure v))
(fm-bind [mv mf]
(if (= :pure (:t mv))
(mf (:v mv)) ;; Pure a >>= f = f a
(impure ;; Free fa >>= f = Free (fmap (>>=f) fa)
((fmap (fn [lmv] (fm-bind lmv mf))) (:v mv)))))
]
{
:m-result fm-result
:m-bind fm-bind
:m-zero ::undefined
:m-plus ::undefined
}
)
)
来自Why free monads matter的例子:
玩具语言的定义。
;; Toy language
;;
(defn output [c n] [{:t :output :v c}, n])
(defn bell [n] [{:t :bell}, n])
(defn done [] [{:t :done}, nil])
(defn toy-fmap [f]
(fn [[e c]]
(if (= :done (:t e))
[e c]
[e (f c)]
))
)
玩具语言monad的定义+辅助函数
;;
(def tt-monad
(free-monad toy-fmap))
(defn liftF [toy]
(impure ((toy-fmap (fn [c] (pure c))) toy))
)
(defn m-output [x] (liftF (output x nil)))
(defn m-bell [] (liftF (bell nil)))
(defn m-done [] (liftF (done)))
(defn f-m-done [_] (m-done))
检查一些规则:
;; return "a" >>= output is Free(Output "a", ())
;; {:t :impure, :v [{:t :output, :v \a} {:t :pure, :v nil}]}
(with-monad tt-monad
(m-bind (m-result \a) m-output)
)
;; output "a" >>= return is Free(Output "a", ())
;; {:t :impure, :v [{:t :output, :v \a} {:t :pure, :v nil}]}
(with-monad tt-monad
(m-bind (m-output \a) m-result)
)
;; ((output 'A' >> done) >> output 'C')
(with-monad tt-monad
(m-bind (m-bind (m-output \a) f-m-done) (fn [_] (m-output \c))))
;;(output 'A' >> (done >> output 'C')) is output 'A' Done
(with-monad tt-monad
(m-bind (m-output \a) (fn [x] (m-bind (m-done) (fn [_] (m-output \c))))))
就数据结构的可读性而言,这可以得到很大改善。 评论和改进非常受欢迎。
答案 2 :(得分:0)
这些是两个很棒的答案,直接回答你的问题,应该推迟到。
我还在学习自己,并希望通过提及免费Monads经常伴随口译员来扩展这个问题,以下是我为@ cotarmanach提到的免费monad建造的一个简单的。
这可以插件和播放,为以下domonad
表达式构建的数据结构提供打印解释器:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INTERPRETER
;;
(def pavlov (domonad tt-monad [a (m-output "hi there pavlov")
b (m-bell)
c (m-output "howdya like me now?")
d (m-bell)
e (m-bell)
f (m-bell)
g (m-done)]
g))
(defn bell-interpreter [program]
(let [{ft :t ; free type: impure or pure?
[{t :t
v :v} next] :v} program]
(if (= :pure ft)
"error, shouldn't have a `pure` type"
(case t
:output (do (prn v) (bell-interpreter next))
:bell (do (prn "rinnnng") (bell-interpreter next))
:done "done!"
:otherwise "uh oh, that's not a type I recognize"))))
对于每个节点,请检查它是Free/Impure
还是Pure
。
如果它是Impure
,那么它就是我们的“已定义”类型之一,因此我们可以解释它。为每个“类型”创建一个案例,并确保调用此递归数据类型的下一个成员。