【问题标题】:Monad equivalent in RubyRuby 中的 Monad 等价物
【发布时间】:2011-02-12 03:42:29
【问题描述】:

在 Ruby 中,monad 的等效构造是什么?

【问题讨论】:

  • 一个单子。为什么会有所不同?
  • 有关异常处理,请查看我的 gem github.com/pzol/monadic中的示例

标签: ruby functional-programming monads


【解决方案1】:

精确的技术定义:在 Ruby 中,monad 可以是任何具有 bindself.unit 方法定义的类,使得对于所有实例 m:

m.class.unit[a].bind[f] == f[a]
m.bind[m.class.unit] == m  
m.bind[f].bind[g] == m.bind[lambda {|x| f[x].bind[g]}]

一些实际例子

一个非常简单的 monad 示例是惰性 Identity monad,它在 Ruby(一种严格的语言)中模拟惰性语义:

class Id
  def initialize(lam)
    @v = lam
  end

  def force
    @v[]
  end

  def self.unit
    lambda {|x| Id.new(lambda { x })}
  end

  def bind
    x = self
    lambda {|f| Id.new(lambda { f[x.force] })}
  end
end

使用它,您可以以一种惰性的方式将 proc 链接在一起。例如,在下面,x 是一个“包含”40 的容器,但直到第二行才执行计算,这一事实证明了 puts 语句直到 force 才输出任何内容被称为:

x = Id.new(lambda {20}).bind[lambda {|x| puts x; Id.unit[x * 2]}]
x.force

一个有点类似但不那么抽象的例子是从数据库中获取值的 monad。假设我们有一个类Query 和一个run(c) 方法,它接受一个数据库连接c,以及一个Query 对象的构造函数,它接受一个SQL 字符串。所以DatabaseValue 代表一个来自数据库的值。 DatabaseValue 是一个单子:

class DatabaseValue
  def initialize(lam)
    @cont = lam
  end

  def self.fromQuery(q)
    DatabaseValue.new(lambda {|c| q.run(c) })
  end

  def run(c)
    @cont[c]
  end

  def self.unit
    lambda {|x| DatabaseValue.new(lambda {|c| x })}
  end

  def bind
    x = self
    lambda {|f| DatabaseValue.new(lambda {|c| f[x.run(c)].run(c) })}
  end
end

这将允许您通过单个连接链接数据库调用,如下所示:

q = unit["John"].bind[lambda {|n|
  fromQuery(Query.new("select dep_id from emp where name = #{n}")).
    bind[lambda {|id|
      fromQuery(Query.new("select name from dep where id = #{id}"))}].
        bind[lambda { |name| unit[doSomethingWithDeptName(name)] }]

begin
  c = openDbConnection
  someResult = q.run(c)
rescue
  puts "Error #{$!}"
ensure
  c.close
end

好吧,那你到底为什么要这么做?因为有一些非常有用的函数可以编写一次所有 monad。因此,只要您简单地实现 unitbind,您通常会一遍又一遍地编写的代码可以用于任何 monad。例如,我们可以定义一个 Monad mixin,为所有此类类赋予一些有用的方法:

module Monad
  I = lambda {|x| x }

  # Structure-preserving transform that applies the given function
  # across the monad environment.
  def map
    lambda {|f| bind[lambda {|x| self.class.unit[f[x]] }]}
  end

  # Joins a monad environment containing another into one environment.
  def flatten
    bind[I]
  end

  # Applies a function internally in the monad.
  def ap
    lambda {|x| liftM2[I,x] }
  end

  # Binds a binary function across two environments.
  def liftM2
    lambda {|f, m|
      bind[lambda {|x1|
        m.bind[lambda {|x2|
          self.class.unit[f[x1,x2]]
        }]
      }]
    }
  end
end

这反过来又让我们做更多有用的事情,比如定义这个函数:

# An internal array iterator [m a] => m [a]
def sequence(m)
  snoc = lambda {|xs, x| xs + [x]}
  lambda {|ms| ms.inject(m.unit[[]], &(lambda {|x, xs| x.liftM2[snoc, xs] }))}
end

sequence 方法接受一个在 Monad 中混合的类,并返回一个函数,该函数接受一个单子值数组并将其转换为一个包含数组的单子值。它们可以是Id 值(将标识数组转换为包含数组的标识),或DatabaseValue 对象(将查询数组转换为返回数组的查询),或函数(将函数数组转换为返回数组的函数),或数组(将数组数组由内向外翻转),或解析器,延续,状态机,或任何其他可能混入Monad模块的东西(事实证明,几乎所有数据结构都是如此)。

【讨论】:

  • 很棒的解释!只是一个问题:这似乎是 Monads 的一个非常实用的实现。在更加面向对象的实现中,unit 不就是new 吗? (实际上,我现在看到类型并不完全匹配。所以,更有趣的问题是:有没有办法让unit 成为new?当我们这样做时,面向对象会是什么样子反过来:让new 表现得像unit?)
  • 好吧,如果new 满足左右单位定律,那就等价了。不幸的是,方法、过程和块在 Ruby 中并不是完全一样的,重要的是,new 是一个副作用。我认为我在这里展示的是 Ruby 中单子的漂亮“OO”编码。还有另一种表达相同数据结构的方法,除了 Proc 之外根本不使用任何类,这将是“非常实用”的实现。反正我认为“面向对象”没有任何确切的含义,所以我采取不去想它的立场。
  • 好的,所以你在第一个例子之后就失去了我,因为你的 Id 课程不遵循你的“精确技术定义”。使用a = 10m = Id.new(lambda { 5 })f = lambda { |x| x.even? ? x/2 : 0 }g = lambda { |x| x * 2 },所有三个条件都失败。我能得到的最接近的是m.class.unit[a].bind[f].force == f[a]m.bind[m.class.unit].force.force == m.force 和在0 上调用bind 时出现的第三个错误。
  • Kache:您的f 类型错误。它应该返回一个Id
  • 那么如果你已经有一个现有的类Foo,它有一组函数 FooFuncs 在Foos 上运行,那么使用 monads 组合 FooFuncs 将需要定义一个包装 @987654366 的 FooMonad 类@ 并将每个 FooFunc 重新实现为 FooMonad.wrap(FooFunc(Foo))?例如对于 FixNum,您必须重新实现 +、*、/、even? 等,如 plus_wrapped、mult_wrapped、div_wrapped、even_wrapped 等?
【解决方案2】:

加上我的两分钱,我想说 hzap 误解了 monad 的概念。 它不仅仅是一个«类型接口»或一个«提供某些特定功能的结构»,它还不止于此。 它是一个抽象结构,提供操作(绑定 (>>=) 和单元 (return)),正如 Ken 和 Apocalisp 所说,遵循严格的规则。

如果您对 monad 感兴趣并且想了解更多关于它们的信息,而不是这些答案中所说的几件事,我强烈建议您阅读:Monads for functional programming (pdf),作者为 Wadler。

再见!

PS:我知道我没有直接回答你的问题,但 Apocalisp 已经回答了,我认为(至少希望)我的精确度是值得的

【讨论】:

    【解决方案3】:

    Monad 不是语言结构。它们只是实现特定接口的类型,并且由于 Ruby 是动态类型的,因此任何在数组中实现类似 collect 的类、连接方法(如 flatten 但仅展平一个级别)以及可以包装任何东西,都是一个单子。

    【讨论】:

    • 它必须正确地实现它们,以便它们遵守单子定律。
    【解决方案4】:

    根据以上答案:

    您可能有兴趣查看Rumonade, a ruby gem which implements a Monad mix-in for Ruby

    Romande 是作为 mix-in 实现的,因此它希望它的宿主类实现 self.unit#bind(以及可选的 self.empty)方法,并会为您完成剩下的工作。

    您可以将它用于map 而不是Option,就像您在Scala 中习惯的那样,您甚至可以得到一些不错的multiple-failure return values from validations,这是Scalaz 的验证类。

    【讨论】:

      猜你喜欢
      • 2012-04-07
      • 1970-01-01
      • 1970-01-01
      • 2018-11-10
      • 2011-07-11
      • 2016-06-21
      • 2010-09-20
      • 1970-01-01
      • 2015-01-07
      相关资源
      最近更新 更多