A számítógép-programozásban a monád a funkcionális paradigma tervmintája. Definiálja, hogyan használhatók együtt függvények, akciók, be- és kimenetek ahhoz, hogy generikus típust építsenek fel. Szerkezete a következő: Adattípust definiál, és megadja, hogyan kombinálhatók ennek értékei. Függvényeket hoz létre, amelyek használják a megadott adattípust, és akciókba csoportosítja őket, az első pont szabályai szerint.
A monád tartalmazhatja a benne definiált adattípus értékeit, a hozzá tartozó specifikus számításokkal, amelyek tipikusan a típus speciális eseteit kezelik. Például a Maybe monád változókat burkol, amiknek lehet null értékük, vagy opcionális típust reprezentálnak, és automatikusan biztosítja, hogy ne adódjanak át null értékek olyan függvényeknek, amelyek nem tudják őket kezelni. Ezzel alternatívát ad a hagyományos kivételkezeléssel (throw-catch) szemben. Egy másik példa a List monád, ahol az üres lista a monádban megadott konstans, és a cons operátor fejelemként hozzáfűz a listához egy elemet.
A monád a számításokat szekvenciális szerkezetben írja le; a monád határozza meg, hogy mi történik az utasítások összekapcsolásakor. Ez lehetővé teszi csővezetékek építését, amelyek lépések (akciók) sorozatával dolgoznak fel adatokat, ahol minden akciót dekorálnak a monád által definiált további feldolgozási szabályokkal. A monád kötelezően definiál két operátort, a return és a bind operátorokat. A return operátor létrehoz értékeket, a bind operátor kapcsolja össze az akciókat, ez utóbbit kísérik a monád törvények, amelyek szükségesek ahhoz, hogy a bind operátor általi összekapcsolás megfelelően működjön.
A monádok lehetővé tesznek egy olyan programozási stílust, amiben a programokat jól komponálható részekből hozzák létre, rugalmasan komponálva a lehetséges akciókat, amik egy adott adattípuson működnek. Emiatt a monádokat programozható pontosvesszőnek is nevezik. Több programozási nyelvben pontosvessző operátor kapcsol össze utasításokat. A monád hasonlítható futószalaghoz is, ami az egyes lépések között szállítja az adatokat.
Tisztán funkcionális programok használhatnak monádokat arra, hogy szekvenciális eljárásokat építsenek fel, az imperatív-strukturált programozási paradigmákhoz hasonlóan. Sok ismert programozási fogalom leírható monádokkal anélkül, hogy rontanák az átláthatóságot, például a mellékhatások, mint az írás-olvasás, értékadás, kivételkezelés, parszolás, nemdeterminisztikusság, konkurencia, folytatások, vagy tartományspecifikus nyelvek. Monádokkal mindezek definiálhatók tisztán funkcionális módon, anélkül, hogy ezzel ki kellene terjeszteni a nyelv szemantikáját. Funkcionális programozási nyelvek, mint a Haskell szabványosan tartalmazzák a monádokat, ami lehetővé teszi, hogy a programozók újrahasználják formális definíciók hosszabb szakaszait, és több különböző könyvtárban használják újra ugyanazokat az interfészeket függvények kombinálására.
A fogalom és elnevezése a kategóriaelméletből származik, ahol a monádok a funktorok speciális fajtája, leképezések kategóriák között. A funkcionális programozásban a kategóriaelmélet erős monádjai jelennek meg. Cikkünkben a monádokat a Haskellen keresztül mutatjuk be, egyéb programnyelvi terminológiát, ötleteket és példákat is fogadunk.
Áttekintés Egy monádot egy típuskonstruktor operátor, és két művelet, a kötés (bind), és a return (nevezik unitnak is) hoz létre. A return unáris művelete vesz egy egyszerű típusértéket, és a konstruktort használva konténerbe teszi, ezzel monadikus értéket hozva létre, például M a. A kötés bináris művelete, jelben >>=, egy monadikus értéket és egy függvényt, ami felhasználja az értéket. A kötés kivonja a beburkolt értéket, és átadja a függvénynek. A függvény által létrehozott értéket monadikussá teszi, ami így átadható a következő kötés operátornak.
Ezekkel az elemekkel létrehozható egy csővezeték (szekvencia), ami több bind operátort hív egymás után. Minden függvényhívás a kivonatolt értéket használja, és a bind operátor kezeli a visszaadott monadikus értéket, ami a következő lépésnek adódik át. Két függvényhívás között a bind operátor további átalakításokat is végezhet, amiket a meghívott függvény nem csinált. Ez lehetővé teszi a végrehajtás pontosabb ellenőrzését, például hogy egy függvényt csak bizonyos esetekben hívjon meg, vagy a függvényhívásokat bizonyos sorrendben hajtsa végre.
Például a következő kód definiál egy 1=x//y biztonságos osztás műveletet, ami a Maybe monád és konstruktorainak segítségével elkerüli a nullával való osztást. A két konstruktor közül a Just jelzi a művelet eredményét, és a Nothing a művelet sikertelenségét. A monadikus értékek alakja Nothing vagy Just i, ahol i egész. Az x és y monadikus értékekből kivonatolt a és b egyszerű értékeken csak akkor végzi el az osztást, ha b nem nulla.
-- Defining a safe division operator "//" (a "monadic function") using the Maybe monad -- The expression x // y is defined as the x // y = -- ''bind'' binary operator (>>=) of the ''Maybe'' monad x >>= (\a -> -- which takes as parameters 'x' and an anonymous function y >>= (\b -> -- which divides x by y. if b == 0 then Nothing else Just (a / b))) -- The result value has type Maybe (Number)
-- Example usages ("pipelines") Nothing // Just 10 -- This expression returns Nothing (by definition of the ''bind'' operator (>>=) in the Maybe monad) Just 10 // Just 5 -- This expression returns Just 2 (by definition of the // operator) Just 10 // Just 0 -- This expression returns Nothing (by definition of the // operator) (Just 10 // Just 0) // Just 2 -- This expression, a pipeline of two composed "//" calls, returns Nothing
A negyedik példa kifejezésben egy csővezeték két biztonságos osztást kapcsol össze; az első osztás eredménye Nothing, ami átadódik a második biztonságos osztásnak, ekkor a végeredmény is Nothing lesz. Jegyezzük meg, hogy ezt az esetet nem kell a // operátornak kezelnie, mivel ezt a Maybe monádjának bind operátora kezeli. A bind definíciója szerint, ha az x vagy y monadikus érték Nothing, akkor az "if b == 0 ..." kifejezés nem értékelődik ki.
A monádot definiáló műveleteknek több tulajdonságnak is eleget kell tenniük, hogy a monadikus függvényeket (amelyek használják a monádot) gond nélkül össze lehessen kapcsolni. Mivel a monádok további műveleteket illeszthetnek a program logikájába, a monádok aspektusorientált programozási elemnek tekinthetők.De Meuter, Wolfgang. "Monads as a theoretical foundation for AOP". Workshop on Aspect Oriented Programming, ECOOP 1997. A tartománylogikát definiálhatja az alkalmazásprogramozó, míg a szükséges mellékesen meghívott függvényeket egy előre megírt és lefordított monád nyújthatja.
Motiváló példák A Haskell szintaktikus cukrokkal teszi kényelmesebbé a monádok létrehozását. Maga a nyelv is tartalmaz több hasznos monádot, az alábbiakban ezekből mutatunk be. A maybe monád a null értékeket tartalmazó számításokat teszi kezelhetőbbé, az I/O monád pedig a környezettel való interakciót biztosítja. A későbbi Példák szakaszban szerepel majd a JavaScript Writer monádja, amivel külön fájlba lehet írni. Maybe monád A Maybe monád típusa a Maybe t, ami lehet Just t, ez egy értéket reprezentál a t típusból; vagy lehet Nothing, ami nem tartalmaz értéket. A különböző típusokhoz tartozó Nothingok különbözőek. data Maybe t = Just t | Nothing
Ezt a típust használhatjuk ellenőrzött kivételek kezelésére: ha a számításnak egy ponton nincs eredménye, akkor a végeredmény Nothing lesz. A sikeres számítás végeredménye Just x, ahol x a sikeres számítás eredménye.
A következő példában az add függvény két argumentumot ad össze, amelyek típusa Maybe Int. Ha mx és my Just értékek, akkor az eredmény Just (x + y), ha pedig valamelyik Nothing, akkor az eredmény is Nothing. Naiv megvalósításban a függvényben többször szerepel az if Nothing akkor Nothing else do valamit az x értékkel, amit a Just x burkol szerkezet, ami hosszabb függvények esetén zavaró, átláthatatlanná tevő boilerplate kód: add :: Maybe Int -> Maybe Int -> Maybe Int add mx my = case mx of Nothing -> Nothing Just x -> case my of Nothing -> Nothing Just y -> Just (x + y)
Ennek elkerülésére operációkat definiálhatunk, amelyek összeláncolják ezeket a műveleteket. A bind bináris operátor (>>=) az egyik számítás eredményét átadja egy másiknak. Mindkét műveletnél előfordulhat, hogy nincs eredmény. Ha nincs eredmény, akkor a második argumentum, a függvény nem lesz figyelembe véve, és a két függvény egymásutánjának nem lesz eredménye. Ez emlékeztet a vastagvonal operátorra, ami azonban másként működik: ha az első számítás eredménytelen, akkor a maradék eredményét kapjuk, és az első számítás el lesz dobva; különben az első számítás eredményét kapjuk. Ha van eredmény, akkor annak alakja Just x, amiből a következő függvény az x értéket kapja meg, és aminek az eredménye egy Maybe monadikus érték lesz. (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b Nothing >>= _ = Nothing -- A failed computation returns Nothing (Just x) >>= f = f x -- Applies function f to value x
A Just értékkonstruktor visszaad egy értéket anélkül, hogy érintené a számítás állapotát: return :: a -> Maybe a return x = Just x -- Wraps value x, returning a value of type (Maybe a)
Ezzel a példa: add :: Maybe Int -> Maybe Int -> Maybe Int add mx my = -- Adds two values of type (Maybe Int), where each input value can be Nothing mx >>= (\x -> -- Extracts value x if mx is not Nothing my >>= (\y -> -- Extracts value y if my is not Nothing return (x + y))) -- Wraps value (x+y), returning the sum as a value of type (Maybe Int)
A Haskell szintaktikus cukorként lehetővé teszi a do jelölést: add :: Maybe Int -> Maybe Int -> Maybe Int add mx my = do x <- mx y <- my return (x + y)
Ezzel a program úgy néz ki, mintha imperatív paradigmában írták volna, ami az 'mx' és 'my' monadikus értékek Int értékeket adja értékül az ideiglenes 'x' és 'y' változóknak, és kivételt dob, ha valamelyikük Nothing. Ekkor a teljes do kifejezés értéke Nothing. A kivételkezelést a deklaráció adja meg, mivel a paraméterek típusa Maybe Int. A try ... catch blokk helyett a viselkedést a bind operátor definiálja.
Mivel ez egy gyakori operáció, azért a Haskell biztosít egy liftM2 szabványos függvényt, ami vesz két monadikus értéket, és kombinálja őket egy adott függvénnyel, így a példa írható úgy is, mint add :: Maybe Int -> Maybe Int -> Maybe Int add = liftM2 (+)
A liftM2 definíciójának kiírása a fenti do-jelölést használó alakot nyújtja. IO monád Tisztán funkcionális nyelvekben, mint a Haskell, a függvényeknek nem lehet a függvény szemantikájának részeként kívülről látható mellékhatása. Megengedett azonban, hogy jelezze, milyen mellékhatást váltana ki; ezt a leírást a hívó felhasználhatja, amikor jónak látja.Peyton Jones, Simon L.; Wadler, Philip. Imperative Functional Programming. Conference record of the Twentieth Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, Charleston, South Carolina. 1993 Haskell nyelven az IO a akciót reprezentál, ami a típusú értéket ad vissza.
Egy IO értékre akcióként gondolhatunk, aminek argumentuma a rendszer jelenlegi állapota, és visszaad egy új rendszert, aminek állapota a függvény visszatérési értékének megfelelően megváltozott. Például a Haskell szabványos könyvtárában a doesFileExist és a removeFile függvények típusa doesFileExist :: FilePath -> IO Bool removeFile :: FilePath -> IO ()