Cela fait un an que j'ai posté cette question. Après l'avoir posté, je me suis plongé dans Haskell pendant quelques mois. Je l'ai énormément apprécié, mais je l'ai mis de côté juste au moment où j'étais prêt à me plonger dans les Monades. Je suis retourné au travail et me suis concentré sur les technologies dont mon projet avait besoin.
C'est plutôt cool. C'est un peu abstrait cependant. J'imagine que les gens qui ne savent pas quelles monades sont déjà confus en raison du manque d'exemples réels.
Alors laissez-moi essayer de me conformer, et juste pour être vraiment clair, je vais faire un exemple en C #, même si cela aura l'air moche. Je vais ajouter l'équivalent Haskell à la fin et vous montrer le sucre syntaxique Haskell cool qui est là où, IMO, les monades commencent vraiment à devenir utiles.
D'accord, donc l'une des monades les plus faciles s'appelle la "Monade peut-être" dans Haskell. En C #, le type Maybe est appelé Nullable<T>
. C'est fondamentalement une classe minuscule qui encapsule simplement le concept d'une valeur qui est soit valide et a une valeur, soit est "nulle" et n'a aucune valeur.
Une chose utile à coller à l'intérieur d'une monade pour combiner des valeurs de ce type est la notion d'échec. C'est-à-dire que nous voulons pouvoir regarder plusieurs valeurs Nullable et retourner null
dès que l'une d'entre elles est NULL. Cela peut être utile si, par exemple, vous recherchez beaucoup de clés dans un dictionnaire ou quelque chose du genre, et que vous souhaitez à la fin traiter tous les résultats et les combiner d'une manière ou d'une autre, mais si l'une des clés n'est pas dans le dictionnaire, vous voulez revenir null
pour tout. Il serait fastidieux d'avoir à vérifier manuellement chaque recherche
null
et retour, afin que nous puissions cacher cette vérification à l'intérieur de l'opérateur de liaison (qui est en quelque sorte le point des monades, nous masquons la comptabilité dans l'opérateur de liaison, ce qui rend le code plus facile à utiliser car on peut oublier les détails).
Voici le programme qui motive le tout (je définirai le
Bind
plus tard, c'est juste pour vous montrer pourquoi c'est sympa).
class Program
{
static Nullable<int> f(){ return 4; }
static Nullable<int> g(){ return 7; }
static Nullable<int> h(){ return 9; }
static void Main(string[] args)
{
Nullable<int> z =
f().Bind( fval =>
g().Bind( gval =>
h().Bind( hval =>
new Nullable<int>( fval + gval + hval ))));
Console.WriteLine(
"z = {0}", z.HasValue ? z.Value.ToString() : "null" );
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
Maintenant, ignorez un instant qu'il existe déjà un support pour faire cela pour Nullable
en C # (vous pouvez ajouter des entiers Nullable ensemble et vous obtenez NULL si l'un ou l'autre est nul). Supposons qu'il n'y ait pas de telle fonctionnalité et que ce soit juste une classe définie par l'utilisateur sans magie particulière. Le fait est que nous pouvons utiliser la Bind
fonction pour lier une variable au contenu de notre Nullable
valeur, puis prétendre qu'il ne se passe rien d'étrange, et les utiliser comme des entiers normaux et simplement les additionner. Nous terminerons le résultat dans une annulable à la fin, et que annulable sera soit nulle (si l' un des f
, g
ou h
renvoie NULL) ou il sera le résultat de la somme f
, g
eth
ensemble. (Ceci est analogue à la façon dont nous pouvons lier une ligne dans une base de données à une variable dans LINQ, et faire des choses avec elle, en sachant que l' Bind
opérateur s'assurera que la variable ne recevra que des valeurs de ligne valides).
Vous pouvez jouer avec cela et changer n'importe lequel de f
, g
et h
pour retourner null et vous verrez que le tout retournera null.
Il est donc clair que l'opérateur de liaison doit faire cette vérification pour nous, et renflouer en retournant null s'il rencontre une valeur nulle, et sinon transmettre la valeur à l'intérieur de la Nullable
structure dans le lambda.
Voici l' Bind
opérateur:
public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f )
where B : struct
where A : struct
{
return a.HasValue ? f(a.Value) : null;
}
Les types ici sont exactement comme dans la vidéo. Il prend une M a
( Nullable<A>
dans la syntaxe C # pour ce cas), et une fonction de a
à
M b
( Func<A, Nullable<B>>
dans la syntaxe C #), et il renvoie un M b
( Nullable<B>
).
Le code vérifie simplement si le nullable contient une valeur et si c'est le cas, l'extrait et le transmet à la fonction, sinon il renvoie simplement null. Cela signifie que l' Bind
opérateur gérera toute la logique de vérification de null pour nous. Si et seulement si la valeur que nous appelons
Bind
est non nulle alors cette valeur sera "transmise" à la fonction lambda, sinon nous sortons rapidement et l'expression entière est nulle. Cela permet au code que nous écrire en utilisant la monade être entièrement libre de ce comportement de vérification nul, nous utilisons simplement Bind
et d' obtenir une variable liée à la valeur à l' intérieur de la valeur monadique ( fval
,
gval
et hval
dans le code exemple) et nous pouvons les utiliser en toute sécurité dans la connaissance qui Bind
se chargera de les vérifier pour null avant de les transmettre.
Il existe d'autres exemples de choses que vous pouvez faire avec une monade. Par exemple, vous pouvez Bind
demander à l' opérateur de prendre en charge un flux d'entrée de caractères et de l'utiliser pour écrire des combinateurs d'analyseurs. Chaque combinateur d'analyseur peut alors être complètement inconscient de choses comme le back-tracking, les échecs de l'analyseur, etc., et simplement combiner des analyseurs plus petits ensemble comme si les choses n'iraient jamais mal, sachant qu'une mise en œuvre intelligente Bind
trie toute la logique derrière bits difficiles. Ensuite, peut-être que quelqu'un ajoute la journalisation à la monade, mais le code utilisant la monade ne change pas, car toute la magie se produit dans la définition de l' Bind
opérateur, le reste du code est inchangé.
Enfin, voici l'implémentation du même code dans Haskell ( --
commence une ligne de commentaire).
-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a
-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x
-- the "unit", called "return"
return = Just
-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
g >>= ( \gval ->
h >>= ( \hval -> return (fval+gval+hval ) ) ) )
-- The following is exactly the same as the three lines above
z2 = do
fval <- f
gval <- g
hval <- h
return (fval+gval+hval)
Comme vous pouvez le voir, la belle do
notation à la fin le fait ressembler à du code impératif simple. Et en effet, c'est par conception. Les monades peuvent être utilisées pour encapsuler tous les éléments utiles de la programmation impérative (état mutable, IO, etc.) et utilisées en utilisant cette belle syntaxe de type impératif, mais derrière les rideaux, ce ne sont que des monades et une implémentation intelligente de l'opérateur de liaison! Ce qui est cool, c'est que vous pouvez implémenter vos propres monades en implémentant >>=
et return
. Et si vous le faites, ces monades pourront également utiliser la do
notation, ce qui signifie que vous pouvez essentiellement écrire vos propres petits langages en définissant simplement deux fonctions!