Équivalent monade en Ruby
Que serait une construction équivalente d'une monade dans Ruby?
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).
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
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.
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.