Clojure中等价于Haskell的<-的是什么?

3

我正在尝试理解IO monad以及在Haskell代码中经常看到的<-语法。我看到它被用于多种数据类型,包括数组和IO。

如果我要自己指定一个等效操作,那么Clojure中的等效操作是什么?


7
请注意,<-(以及一般而言的 do 表示法)根本不是运算符,而是 (>>=) 运算符(发音为“bind”)的语法糖。由于 (>>=)Monad 类的一个类方法,因此它具有非常不同的实现,取决于底层数据类型。 - kosmikus
它在IO方面是如何实现的? - zcaudate
它并没有涉及<-,但也许这篇关于Clojure和Haskell函数的帖子会有所帮助:http://productivedetour.blogspot.com.es/2013/05/haskell-equivalents-of-some-clojure.html - danidiaz
1
稍作澄清:<-不能与“几乎任何东西”一起使用,只能与单子一起使用。 - Matt Fenwick
3个回答

6

Do-notation只是标准monad操作的语法糖。例如,如果你有类似这样的东西:

do
  x <- someMonad
  return (someFunction x)

这相当于这个:
someMonad >>= \x -> return (someFunction x)

因此,使用众多单子库之一的Clojure的等效语法可能如下所示:

(m-bind some-monad (fn [x] (m-result (some-function x))))

我正在寻找一个具体的例子,说明如何在Clojure中使用printf和readline实现绑定(bind)到io monad。 - zcaudate

3

我认为 Chuck 已经回答了你的主要问题,但是如果你想探索使用 algo.monads 作为示例在 Clojure 中可以如何实现单子操作的方式,可以参考以下内容:

(domonad state-m
  [_ (set-state "foo")
   x (fetch-state)]
  x)

这与Haskell的相当(好吧,几乎相当,详见下文)

do
  _ <- put "foo" -- see below for a comment on this
  x <- get
  return x

algo.monads中,<-消失了,因为实际上它在每一行都被暗示了。
关于上面的"几乎"和_:在Clojure中,_实际上并不神奇,它将绑定到由set-state返回的值,但使用此符号作为不关心的本地名称是惯用法。当然,在Haskell中,通常只需要写put "foo"而不是_ <- put "foo"

谢谢Michal。IO单子的实现方式和状态单子一样吗? - zcaudate
1
@zcaudate,是的,在某种程度上可能是这样...考虑阅读SPJ的论文关于一个主题,它可以提供一些有用的关于IO单子的见解。 - ownclo

3
使用algo.monads,我们可以轻松地(尽管没有必要)定义一个IO单子。
在Haskell中,IO单子是type IO a = World -> (a, World)。很方便将其视为一种动作——需要传入世界,执行某些操作,并返回值和世界。
使用向量代替元组,在Clojure中,一个IO动作(IO单子的单子值)看起来像这样:
(fn [world]
    ; some stuff
    [value world])

为了做一些有趣的事情,我们需要执行几个操作:get-charput-charget-char是一个动作,它获取输入,读取一个字符,并返回该字符及其值作为输出:
(defn read-char
  []
  (-> *in* .read char))

(defn get-char
  [world]
  [(read-char) world])

put-char 函数接受一个字符,创建一个动作,并在给定世界的情况下打印该字符并返回一些(无关紧要的)值:

(defn put-char
  [c]
  (fn [world]
    (print c)
    [nil world]))

请注意,要发生一个动作,我们必须提供一个世界。例如,(put-char \a)会返回一个动作;((put-char \a) :world)将调用该动作,打印出a并返回[nil :world]
组合这些动作可能是一个非常混乱的过程。例如,如果您想要获得一个字符,然后将其打印出来,您必须调用get-char,解包其字符和世界,在put-char中创建该字符的动作,然后将世界传递给该动作。
另一方面,如果我们定义一个单子,我们将免费得到domonad(类似于Haskell的do)。这种语法糖减轻了解压/打包样板文件。我们只需要一些函数:m-resultm-bindm-zerom-plus也很方便,但不是必需的)。 m-result(Haskell中的return)接受一个值并将其封装为一个动作:
(fn [v]
  (fn [world]
    [v world]))

m-bind(在Haskell中是>>=)接受一个操作和一个函数,该函数将常规值转换为操作,"解包"该值通过调用操作,并将函数应用于其上。在IO单子中,看起来像这样:

(fn [io f]
  (fn [world]
    (let [[v new-world] (io world)]
      ((f v) new-world))))

因此,使用algo.monads,我们可以定义io-m如下:
(defmonad io-m
  [m-result (fn [v]
              (fn [world]
                [v world]))

   m-bind (fn [io f]
            (fn [world]
              (let [[v new-world] (io world)]
               ((f v) new-world))))])

现在我们已经有了原始的IO操作和组合它们的方法,我们可以创建更有趣的操作。请注意,Haskell的解包运算符 (<-) 是隐式的,结果会自动用 m-result 包装,因此我们不需要使用Haskell的 return 语句来终止表达式:

(declare get-rest-of-line)

(def get-line
  (domonad io-m
    [c get-char
     line (if (= c \newline)
                (m-result "")
                (get-rest-of-line c))]
     line))

 (defn get-rest-of-line
   [c]
   (domonad io-m
     [cs get-line]
     (str c cs)))

 (defn put-line
   [s]
   (if (seq s)
     (domonad io-m
       [_ (put-char (first s))
        _ (put-line (subs s 1))]
       _)
      (put-char \newline)))

最后,我们可以根据这些IO操作编写程序:
(def run-program
  (domonad io-m
    [line get-line
     :let [reversed-line (->> line reverse (apply str))]
     _ (put-line reversed-line)]
    _))

(run-program :world)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接