De monads is niet een bandje dat een minuut heeft gespeeld in De Wereld Draait Door. De monad is gebaseerd op wiskundige concepten uit de categorietheorie en heeft zijn oorsprong in de functionele programmeertaal Haskell. Waarschijnlijk is voor veel .NET-ontwikkelaars de monad echter een onbekend begrip. Omdat .NET steeds meer functionele concepten ondersteunt, kan het ook voor hem/haar interessant zijn te weten wat een monad is.
In dit artikel wordt kort uitgelegd wat de monad kan bieden. Theoretische concepten zullen daarbij zoveel mogelijk worden vermeden. Daarna volgt er een praktisch voorbeeld in C# en wordt er een link met LINQ gelegd. Er wordt afgesloten met een kort voorbeeld in F#.
Problemen oplossen
Een methode om een probleem op te lossen is gebruik te maken van een oplossing uit een ander domein, het recept:
1. Beschrijf het probleem in het originele domein
2. Vertaal het probleem van het originele domein in doeldomein
3. Los het probleem in het doeldomein op
4. Vertaal de oplossing weer terug naar het originele domein
Computersimulaties zijn een voorbeeld van het gebruik van dit recept. De werkelijkheid is het originele domein en het computermodel het doeldomein.
De monad gebruikt ook dit recept. De definitie van de monad eist dat er twee functies worden geïmplementeerd: Return en Bind. Deze functies vertalen op een speciale manier waarden van het originele domein naar het doeldomein en terug. In het voorbeeld zal worden beschreven op welke wijze dit gebeurd.
Er zijn verschillende soorten monads. In de literatuur en op het internet zijn hiervan classificaties te vinden. In het voorbeeld maken we gebruik van de Maybe-monad. De Maybe-monad is een wrapper-class om een waarde en geeft aan wanneer deze waarde gebruikt kan worden. Andere veel genoemde voorbeelden van monads zijn de log-monad en de state-monad.
Dry boilerplate code
In het voorbeeld zullen we zien dat de monad kan worden gebruikt om “boilerplate code” op één plaats vast te leggen. “Boilerplate code” kan worden omschreven als de code die keer op keer opnieuw geschreven moet worden, maar weinig variatie bevat. Deze soort code gaat in tegen het DRY-principe. Je moet jezelf niet herhalen, in het Engels: Don't repeat yourself (DRY).
Er zijn verschillende mogelijkheden om herhaling te voorkomen:
• Code in methodes plaatsen;
• Gebruik maken van objecthiërarchieën;
• In .NET gebruik maken van generics.
De monad is dus een andere techniek om “boilerplate code” op één plaats vast te leggen. In de monad kan inspectiecode worden vastgelegd, waardoor kan worden voorkomen dat we onszelf herhalen.
Beoordelingen als voorbeeld
Stel dat we een code moeten schrijven om een beoordeling vast te leggen. Bijvoorbeeld een beoordeling van een film of een fles wijn. In ons geval bestaat de beoordeling uit een aantal sterren.
public class Beoordeling
{
private int _aantalSterren;
public int AantalSterren
{
get { return _aantalSterren; } }
public Beoordeling(int aantalSterren)
{
_aantalSterren = aantalSterren; }
public override string ToString()
{
return new string('*', _aantalSterren); }
}
Listing 1: Beoordeling
Verder is er een eis dat we twee beoordelingen kunnen middelen:
private static Beoordeling Gemiddeld(Beoordeling b1, Beoordeling b2)
{
return new Beoordeling((b1.AantalSterren + b2.AantalSterren)/2);
}
Listing 2: Gemiddelde
De invoer:
Beoordeling beoordeling1 = new Beoordeling(1);
Console.WriteLine(string.Format("Beoordeling 1: {0}", beoordeling1));
Beoordeling beoordeling3 = new Beoordeling(3);
Console.WriteLine(string.Format("beoordeling 3: {0}", beoordeling3));
Beoordeling beoordelingGemiddeld = Gemiddeld(beoordeling1, beoordeling3);
Console.WriteLine(string.Format("Beoordeling gemiddeld: {0}", beoordelingGemiddeld))
Listing 3: Voorbeeld van een gemiddelde.
geeft:
Figuur 1: Het gemiddelde van twee beoordelingen
In bovenstaand voorbeeld is het probleem van het middelen van twee beoordelingen vertaald in het middelen van het aantal sterren (in integers). We weten hoe we integers kunnen middelen; aantallen optellen en delen door twee. De vertaling van beoordelingen naar integers en terug vindt echter impliciet plaats.
Stel nu dat er ook een eis is dat we het maximum willen bepalen van twee beoordelingen. Dan kunnen we weer impliciet de transformatie doorvoeren of we kunnen een andere mogelijkheid bekijken: de monad.
Het recept in code
De definitie van een monad bevat twee functies: Bind en Return. Laten we met Return beginnen, dit is de simpelste functie van de twee. De naam Return heeft geen enkele relatie met het C#-keyword return. Return is stap 4 van het recept. Vanuit het doeldomein vertaalt Return de waarde terug naar het originele domein.
In ons beoordelingsvoorbeeld is Return als een extensiemethode voor een integer geïmplementeerd:
public static Beoordeling Return(this int aantal)
{
return new Beoordeling(aantal);
}
Listing 4: Return
Bind beschrijft stap 2 uit het recept. Bind is echter complexer dan Return. Deze functie heeft twee parameters: een waarde in het originele domein en een functie f die waarden vanuit het doeldomein overzet in waarden van het originele domein. De functie f bestaat in de praktijk vaak uit een transformatie in het doeldomein; de actie die we daar willen uitvoeren om vervolgens het resultaat met behulp van de Return terug te vertalen naar het originele domein. De functie f is een voorbeeld van een continuation (een vervolg).
In ons voorbeeld wordt in Bind vastgelegd hoe een beoordeling wordt omgezet in een integer. Daarna wordt met behulp van de functie f de waarde bewerkt en vertaald in een waarde in het doeldomein. Ook Bind is een extensiemethode.
public static Beoordeling Bind(this Beoordeling beoordeling, Func<int, Beoordeling> f)
{
return f(beoordeling.AantalSterren);
}
Listing 5: Bind
Middelen van beoordeling ziet er nu als volgt uit:
private static Beoordeling GemiddeldBind(Beoordeling b1, Beoordeling b2)
{
return b1.Bind(x => b2.Bind(y => ((x + y)/2).Return()));
}
Listing 6: GemiddeldBind
Op deze manier gemiddelde definiëren is complexer dan de originele functie om het gemiddelde te bepalen. Echter een functie die het maximum van twee beoordelingen bepaald is nu snel gemaakt met behulp van de helperfunctie Lift2:
private static Beoordeling Lift2(Beoordeling b1, Beoordeling b2, Func<int, int, int> f)
{
return b1.Bind(x => b2.Bind(y => (f(x, y).Return())));
}
private static Beoordeling MaximumBind(Beoordeling b1, Beoordeling b2)
{
return Lift2(b1, b2, Math.Max);
}
Listing 7: MaximumBind
Elke functie met twee integers als parameters kan nu worden gebruikt om twee beoordelingen te bewerken tot één nieuwe beoordeling.
Interceptie
Stel nu dat er extra eisen worden gesteld:
• Ongeldige beoordelingen zijn beoordelingen waarbij het aantal sterren kleiner is dan nul. Ongeldige beoordelingen mogen wel worden aangemaakt, maar mogen niet worden verwerkt. Deze beoordelingen moeten een foutmelding geven bij bewerking, zoals bij het bepalen van het gemiddelde en het maximum.
• Toppers zijn beoordelingen in de buitencategorie. Als een beoordeling een topper is dan is het gemiddelde van twee beoordelingen ook weer een topper. Het maximum van twee toppers is vanzelfsprekend ook weer een topper.
Er worden twee extra proporties voor een beoordeling gedefinieerd: Geldig en Topper. Als er geen gebruik wordt gemaakt van de monad-functies Return en Bind, dan ziet de code voor het gemiddelde er nu als volgt uit:
private static Beoordeling Gemiddeld(Beoordeling b1, Beoordeling b2)
{
if (!b1.Geldig || !b2.Geldig)
throw new Exception("ongeldige beoordeling");
else if (b1.Topper)
return b1; else if (b2.Topper)
return b2;
else
return new Beoordeling((b1.AantalSterren + b2.AantalSterren)/2);}
Listing 8: Aanpassing van Gemiddeld
En ook de maximum functie moet worden aangepast.
Als we de monad-functies gebruiken, dan is het voldoende Bind aan te passen:
public static Beoordeling Bind(this Beoordeling beoordeling, Func<int, Beoordeling> f)
{
if (!beoordeling.Geldig)
throw new Exception("ongeldige beoordeling"); else if (beoordeling.Topper)
return beoordeling; else
return f(beoordeling.AantalSterren);}
Listing 9: Aanpassing Bind
en kan de overige code (Lift2, Gemiddeld en Maximum) onveranderd blijven en herhalen we onszelf niet.
LINQ sugar
Als de monad-functies zijn gemaakt, dan kan het lastig zijn om met deze complexe functies te werken. De pijn kan een beetje worden verzacht met zoet: sugar. In het geval van C# bestaat deze syntactic sugar uit LINQ.
Wel moet er nog een extra helper-functie SelectMany gedefinieerd worden, deze functie maakt gebruik van Bind en Return:
public static Beoordeling SelectMany(this Beoordeling beoordeling,
Func<int, Beoordeling> f, Func<int, int, int> selector)
{
return beoordeling.Bind(aantal => f(aantal).
Bind(x => selector(aantal, x).Return()));
}
Listing 10: SelectMany
Nu kan LINQ gebruikt worden om ad-hoc queries te maken. Als voorbeeld weer het gemiddelde van beoordeling1 (één ster) en beoordeling3 (drie sterren):
Beoordeling beoordelingLinq =
from x in beoordeling1 from y in beoordeling3 select ((x + y) / 2); Console.WriteLine(string.Format("Beoordeling met LINQ: {0}", beoordelingLinq));
Listing 11: Linq
Figuur 2: Het gemiddelde van twee beoordelingen met LINQ
En als één van de beoordelingen ongeldig of een topper is, dan is het resultaat weer een foutmelding of een topper.
F#
De monad heeft zijn oorsprong in de functionele programmeertalen. De functionele programmeertaal in de .NET is natuurlijk F# en zal daarom worden afgesloten met een vertaling van het voorbeeld in F#.
In F# worden monads computing expressions of workflows genoemd. Deze computing expressions bieden veel meer mogelijkheden dan LINQ in C#. Zo is het bijvoorbeeld niet meer nodig de SelectMany-functie zelf te implementeren. De implementatie van de gemiddelde beoordeling inclusief monad-functies ziet er in F# als volgt uit:
type beoordelingBuilder()=
member x.Bind(beoordeling:Beoordeling,(f:int -> Beoordeling)) = if not(beoordeling.Geldig) then failwithf "ongeldige beoordeling" elif beoordeling.Topper then beoordeling else f(beoordeling.AantalSterren) member x.Return(aantal) = new Beoordeling(aantal)
let beoordeling = new beoordelingBuilder()
let gemiddelde b1 b2 =
beoordeling {
let! x = b1 let! y = b2 return (x + y)/2 }let b1 = new Beoordeling(1)
let b3 = new Beoordeling(3)
let gem = gemiddelde b1 b3
Listing 12: Voorbeeld in F#
Ook als je (nog) onbekend bent met F#, kan je een aantal elementen uit het voorbeeld herkennen:
• De definitie van een monad: beoordelingBuilder;
• de implementatie van Bind met de verwerking van Topper en Geldig;
• de Return functie die het aantal sterren weer omzet in een beoordeling;
• de creatie van beoordeling compution expression;
• de definitie van het gemiddelde van twee beoordelingen.
Het gemiddelde is in het voorbeeld natuurlijk weer twee sterren, hetzelfde resultaat als in C#. En als één van de beoordelingen ongeldig of een topper is, dan is het resultaat weer een foutmelding of een topper.
Als je meer wilt weten over F#, lees dan de F#-artikelen in SDN 99 en SDN 103.
Tot slot
Dit artikel was een korte introductie van de monad voor .NET-ontwikkelaars. De theorie achter de monad is zoveel mogelijk vermeden. Aan de hand van een simpel voorbeeld zagen we waar de monad ons kan helpen. Op het web en in de literatuur zijn meer voorbeelden van monads te vinden en wordt de theorie veel uitgebreider beschreven. Ik heb een blog post geschreven met links voor meer informatie. Daar staat ook een link naar de code die hoort bij dit artikel.
http://ps-a.blogspot.com/2011/09/monads-for-net-developers.html