Монадный эквивалент в Ruby - PullRequest
39 голосов
/ 25 апреля 2010

Что такое эквивалентная конструкция монады в Ruby?

Ответы [ 4 ]

74 голосов
/ 25 апреля 2010

Точное техническое определение : Монада в Ruby будет любым классом с методами bind и self.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]}]

Некоторые практические примеры

Очень простой пример монады - это ленивая монада Identity, которая эмулирует ленивую семантику в 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

Используя это, вы можете ленивым образом соединять вместе цепочки. Например, в следующем примере x является контейнером, «содержащим» 40, но вычисление не выполняется до второй строки, о чем свидетельствует тот факт, что оператор puts ничего не выводит до force называется:

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

Несколько похожим, менее абстрактным примером была бы монада для получения значений из базы данных. Давайте предположим, что у нас есть класс 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

Хорошо, так с какой стати ты это сделал? Потому что есть чрезвычайно полезные функции, которые можно написать один раз для всех монад . Так что код, который вы обычно пишете снова и снова, можно повторно использовать для любой монады, если вы просто реализуете unit и bind. Например, мы можем определить миксин Monad, который наделяет все такие классы некоторыми полезными методами:

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 (который, как оказалось, верно почти для всех структур данных).

6 голосов
/ 25 апреля 2010

Чтобы сложить два моих цента, я бы сказал, что hzap неправильно понял концепцию монад. Это не просто «интерфейс типа» или «структура, обеспечивающая некоторые специфические функции», это намного больше. Это абстрактная структура, обеспечивающая операции (bind (>> =) и unit (return)), которые следуют, как сказали Кен и Apocalisp, строгим правилам.

Если вас интересуют монады и вы хотите узнать о них больше, чем то, что было сказано в этих ответах, я настоятельно рекомендую вам прочитать: Монады для функционального программирования (pdf) от Wadler

Увидимся!

PS: Я вижу, что не отвечаю напрямую на ваш вопрос, но Apocalisp уже ответил, и я думаю (по крайней мере, надеюсь), что мои точности стоили этого

5 голосов
/ 25 апреля 2010

Монады не являются языковыми конструкциями. Это просто типы, которые реализуют определенный интерфейс, и поскольку Ruby динамически типизирован, любой класс, который реализует что-то вроде collect в массивах, метод соединения (например, flatten, но выравнивает только один уровень) и конструктор, который заверните что-нибудь, это монада.

0 голосов
/ 03 мая 2012

Следуя приведенным выше ответам:

Возможно, вас заинтересует Rumonade, рубиновый драгоценный камень, в котором реализован плагин Monad для Ruby .

Romande реализован как смешанный, поэтому он ожидает, что его хост-класс реализует методы self.unit и #bind (и, необязательно, self.empty), и сделает все остальное, чтобы заставить вас работать.

Вы можете использовать его для map над Option, как вы привыкли в Scala, и вы даже можете получить несколько хороших возвращаемых значений с множественными ошибками из проверок , как класс проверки Scalaz's ,

...