Équivalent monade en Ruby

Que serait une construction équivalente d'une monade dans Ruby?

38
demandé sur fig 2010-04-25 22:25:01

4 réponses

La définition technique précise : une monade, en Ruby, serait n'importe quelle classe avec les méthodes bind et self.unit définies de telle sorte que pour toutes les instances 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]}]

Quelques exemples pratiques de

Un exemple très simple d'une monade est la monade d'identité paresseuse, qui émule la sémantique paresseuse dans Ruby (un langage strict):

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

En utilisant ceci, vous pouvez enchaîner les procs ensemble d'une manière paresseuse. Par exemple, dans ce qui suit, x est un conteneur " contenant" 40, mais le calcul n'est pas effectué avant la deuxième ligne, comme en témoigne le fait que l'instruction puts ne produit rien avant que force ne soit appelée:

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

Un exemple un peu similaire, Moins abstrait serait une monade pour extraire des valeurs d'une base de données. Supposons que nous ayons une classe Query avec une méthode run(c) qui prend une connexion de base de données c, et un constructeur d'objets Query qui prend, par exemple, une chaîne SQL. Donc DatabaseValue représente une valeur qui vient à partir de la base de données. DatabaseValue est une monade:

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

Cela vous permettrait de chaîner les appels de base de données via une seule connexion, comme ceci:

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

OK, alors pourquoi diable feriez-vous ça? Parce qu'il y a des fonctions extrêmement utiles qui peuvent être écrites une fois pour toutes les monades . Donc, le code que vous écrivez normalement encore et encore peut être réutilisé pour n'importe quelle monade une fois que vous implémentez simplement unit et bind. Par exemple, nous pouvons définir un mixin de monade qui dote toutes ces classes avec quelques méthodes utiles:

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

Et cela à son tour nous permet de faire des choses encore plus utiles, comme définir cette fonction:

# 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

La méthode sequence prend une classe qui se mélange dans Monad, et renvoie une fonction qui prend un tableau de valeurs monadiques et le transforme en une valeur monadique contenant un tableau. Ils peuvent être Id valeurs (transformer un tableau D'identités en une identité contenant un tableau), ou DatabaseValue objets (transformer un tableau de requêtes en une requête qui renvoie un tableau), ou fonctions (transformer un tableau de fonctions en une fonction qui renvoie un tableau), ou tableaux (transformer un tableau de tableaux à l'intérieur), ou analyseurs, continuations, machines d'état, ou toute autre chose qui pourrait éventuellement mélanger dans le module Monad (ce qui, comme il s'avère, est vrai pour presque toutes les structures de données).

73
répondu Apocalisp 2012-05-03 21:07:38

Pour ajouter mes deux cents, je dirais que hzap a mal compris le concept de monades. Ce n'est pas seulement une "interface de type" ou une " structure fournissant des fonctions spécifiques ", c'est beaucoup plus que cela. C'est une structure abstraite fournissant des opérations (bind (>>=) et unit (return)) qui suivent, comme L'ont dit Ken et Apocalisp, des règles strictes.

Si vous êtes intéressé par les monades et que vous voulez en savoir plus à leur sujet que les quelques choses dites dans ces réponses, je vous conseille fortement de lire: monades pour la programmation fonctionnelle (pdf), par Wadler.

Te Voir!

PS: je vois que je ne réponds pas directement à votre question, mais Apocalisp l'a déjà fait, et je pense (au moins j'espère) que mes précisions en valaient la peine

6
répondu rks 2010-04-25 19:26:50

Les monades ne sont pas des constructions de langage. Ce sont juste des types qui implémentent une interface particulière, et puisque Ruby est typé dynamiquement, toute classe qui implémente quelque chose comme collect dans les tableaux, une méthode de jointure (comme flatten mais n'aplatit qu'un niveau), et un constructeur qui peut envelopper n'importe quoi, est une monade.

5
répondu hzap 2010-04-25 18:35:59

Suite aux réponses ci-dessus:

Vous pourriez être intéressé par Rumonade, une gemme ruby qui implémente un mix-in Monad pour Ruby.

Romande est implémenté en tant que mix-in, donc il s'attend à ce que sa classe hôte implémente les méthodes self.unit et #bind (et éventuellement, self.empty), et fera le reste pour que les choses fonctionnent pour vous.

, Vous pouvez l'utiliser pour map sur Option, que vous êtes habitué à la Scala, et vous pouvez même obtenir quelques belles multiples-non-valeurs de retour de validations , une classe de Validation de la Scalaz.

0
répondu ms-tg 2012-05-03 20:59:27