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 nulldè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 nullpour tout. Il serait fastidieux d'avoir à vérifier manuellement chaque recherche
nullet 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
Bindplus 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 Nullableen 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 Bindfonction pour lier une variable au contenu de notre Nullablevaleur, 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, gou hrenvoie NULL) ou il sera le résultat de la somme f, gethensemble. (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' Bindopé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, get hpour 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 Nullablestructure dans le lambda.
Voici l' Bindopé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' Bindopérateur gérera toute la logique de vérification de null pour nous. Si et seulement si la valeur que nous appelons
Bindest 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 Bindet d' obtenir une variable liée à la valeur à l' intérieur de la valeur monadique ( fval,
gvalet hvaldans le code exemple) et nous pouvons les utiliser en toute sécurité dans la connaissance qui Bindse 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 Binddemander à 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 Bindtrie 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' Bindopé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 donotation à 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 donotation, ce qui signifie que vous pouvez essentiellement écrire vos propres petits langages en définissant simplement deux fonctions!