[oop] Monade en anglais ordinaire? (Pour le programmeur POO sans arrière-plan FP)


Pourquoi avons-nous besoin de monades?

  1. Nous voulons programmer uniquement en utilisant des fonctions . ("programmation fonctionnelle" après tout -FP).
  2. Ensuite, nous avons un premier gros problème. C'est un programme:

    f(x) = 2 * x

    g(x,y) = x / y

    Comment pouvons-nous dire ce qui doit être exécuté en premier ? Comment pouvons-nous former une séquence ordonnée de fonctions (c'est -à- dire un programme ) en n'utilisant que des fonctions?

    Solution: composer des fonctions . Si vous voulez d'abord g , puis f , écrivez simplement f(g(x,y)) . OK mais ...

  3. Plus de problèmes: certaines fonctions peuvent échouer (ie g(2,0) , diviser par 0). Nous n'avons aucune "exception" dans FP . Comment pouvons-nous le résoudre?

    Laisser les fonctions renvoyer deux sortes de choses : au lieu d'avoir g : Real,Real -> Real (fonction de deux réels en un réel), admettons g : Real,Real -> Real | Nothing g : Real,Real -> Real | Nothing (fonction de deux réels en (réel ou rien)).

  4. Mais les fonctions devraient (pour être plus simples) ne rendre qu'une chose .

    Solution: créons un nouveau type de données à renvoyer, un " type de boxe " qui entoure peut-être un réel ou ne soit simplement rien. Par conséquent, nous pouvons avoir g : Real,Real -> Maybe Real . OK mais ...

  5. Qu'advient-il maintenant de f(g(x,y)) ? f n'est pas prêt à consommer un Maybe Real . Et, nous ne voulons pas changer toutes les fonctions que nous pourrions connecter avec g pour consommer un Maybe Real .

    Solution: avons une fonction spéciale pour "connecter" / "composer" / "lien" fonctions . De cette façon, nous pouvons, dans les coulisses, adapter la sortie d'une fonction pour alimenter la suivante.

    Dans notre cas: g >>= f (connecter / composer g à f ). Nous voulons >>= obtenir la sortie de g , l'inspecter et, dans le cas contraire, ne pas appeler f et retourner Nothing ; ou au contraire, extraire le Real encadré et nourrir f avec lui. (Cet algorithme est juste l'implémentation de >>= pour le type Maybe ).

  6. Beaucoup d'autres problèmes surgissent qui peuvent être résolus en utilisant ce même modèle: 1. Utilisez une "boîte" pour codifier / stocker différentes significations / valeurs, et avoir des fonctions comme g qui renvoient ces "valeurs encadrées". 2. Avoir des compositeurs / lieurs g >>= f pour aider à connecter la sortie de g l'entrée f , donc nous n'avons pas besoin de changer f du tout.

  7. Les problèmes remarquables qui peuvent être résolus en utilisant cette technique sont:

    • avoir un état global que toutes les fonctions de la séquence de fonctions ("le programme") peuvent partager: solution StateMonad .

    • Nous n'aimons pas les "fonctions impures": des fonctions qui donnent des résultats différents pour la même entrée. Par conséquent, marquons ces fonctions, en leur faisant retourner une valeur étiquetée / encadrée: IO monad.

Total bonheur


En termes qu'un programmeur POO comprendrait (sans arrière-plan de programmation fonctionnelle), qu'est-ce qu'une monade?

Quel problème résout-il et quels sont les endroits les plus communs où il est utilisé?


Pour clarifier le type de compréhension que je cherchais, disons que vous étiez en train de convertir une application de PF qui avait des monades en une application POO. Que feriez-vous pour transférer les responsabilités des monades à l'application POO?

From a practical point of view (summarizing what has been said in many previous answers and related articles), it seems to me that one of the fundamental "purposes" (or usefulness) of the monad is to leverage the dependencies implicit in recursive method invocations aka function composition (ie when f1 calls f2 calls f3, f3 needs to be evaluated before f2 before f1) to represent sequential composition in a natural way, especially in the context of a lazy evaluation model (that is, sequential composition as a plain sequence, eg "f3(); f2(); f1();" in C - the trick is especially obvious if you think of a case where f3, f2 and f1 actually return nothing [their chaining as f1(f2(f3)) is artificial, purely intended to create sequence]).

This is especially relevant when side-effects are involved, ie when some state is altered (if f1, f2, f3 had no side-effects, it wouldn't matter in what order they're evaluated; which is a great property of pure functional languages, to be able to parallelize those computations for example). The more pure functions, the better.

I think from that narrow point of view, monads could be seen as syntactic sugar for languages that favor lazy evaluation (that evaluate things only when absolutely necessary, following an order that does not rely on the presentation of the code), and that have no other means of representing sequential composition. The net result is that sections of code that are "impure" (ie that do have side-effects) can be presented naturally, in an imperative manner, yet are cleanly separated from pure functions (with no side-effects), which can be evaluated lazily.

This is only one aspect though, as warned here .

In OO terms, a monad is a fluent container.

The minimum requirement is a definition of class <A> Something that supports a constructor Something(A a) and at least one method Something<B> flatMap(Function<A, Something<B>>)

Arguably, it also counts if your monad class has any methods with signature Something<B> work() which preserves the class's rules -- the compiler bakes in flatMap at compile time.

Why is a monad useful? Because it is a container that allows chain-able operations that preserve semantics. For example, Optional<?> preserves the semantics of isPresent for Optional<String> , Optional<Integer> , Optional<MyClass> , etc.

As a rough example,

Something<Integer> i = new Something("a")

Note we start with a string and end with an integer. Pretty cool.

In OO, it might take a little hand-waving, but any method on Something that returns another subclass of Something meets the criterion of a container function that returns a container of the original type.

That's how you preserve semantics -- ie the container's meaning and operations don't change, they just wrap and enhance the object inside the container.

I'll try to make the shortest definition I can manage using OOP terms:

A generic class CMonadic<T> is a monad if it defines at least the following methods:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell

and if the following laws apply for all types T and their possible values t

left identity:

CMonadic<T>.create(t).flatMap(f) == f(t)

right identity

instance.flatMap(CMonadic<T>.create) == instance


instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Exemples :

A List monad may have:

List<int>.create(1) --> [1]

And flatMap on the list [1,2,3] could work like so:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iterables and Observables can also be made monadic, as well as Promises and Tasks.

Commentary :

Monads are not that complicated. The flatMap function is a lot like the more commonly encountered map . It receives a function argument (also known as delegate), which it may call (immediately or later, zero or more times) with a value coming from the generic class. It expects that passed function to also wrap its return value in the same kind of generic class. To help with that, it provides create , a constructor that can create an instance of that generic class from a value. The return result of flatMap is also a generic class of the same type, often packing the same values that were contained in the return results of one or more applications of flatMap to the previously contained values. This allows you to chain flatMap as much as you want:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

It just so happens that this kind of generic class is useful as a base model for a huge number of things. This (together with the category theory jargonisms) is the reason why Monads seem so hard to understand or explain. They're a very abstract thing and only become obviously useful once they're specialized.

For example, you can model exceptions using monadic containers. Each container will either contain the result of the operation or the error that has occured. The next function (delegate) in the chain of flatMap callbacks will only be called if the previous one packed a value in the container. Otherwise if an error was packed, the error will continue to propagate through the chained containers until a container is found that has an error handler function attached via a method called .orElse() (such a method would be an allowed extension)

Notes : Functional languages allow you to write functions that can operate on any kind of a monadic generic class. For this to work, one would have to write a generic interface for monads. I don't know if its possible to write such an interface in C#, but as far as I know it isn't:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad

A monad is an array of functions

(Pst: an array of functions is just a computation).

Actually, instead of a true array (one function in one cell array) you have those functions chained by another function >>=. The >>= allows to adapt the results from function i to feed function i+1, perform calculations between them or, even, not to call function i+1.

The types used here are "types with context". This is, a value with a "tag". The functions being chained must take a "naked value" and return a tagged result. One of the duties of >>= is to extract a naked value out of its context. There is also the function "return", that takes a naked value and puts it with a tag.

An example with Maybe . Let's use it to store a simple integer on which make calculations.

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

Just to show that monads are array of functions with helper operations, consider the equivalent to the above example, just using a real array of functions

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

And it would be used like this:

print (runMyMonad (Just 160) myArray1)

A monad is a data type that encapsulates a value, and to which, essentially, two operations can be applied:

  • return x creates a value of the monad type that encapsulates x
  • m >>= f (read it as "the bind operator") applies the function f to the value in the monad m

That's what a monad is. There are a few more technicalities , but basically those two operations define a monad. The real question is, "What a monad does ?", and that depends on the monad — lists are monads, Maybes are monads, IO operations are monads. All that it means when we say those things are monads is that they have the monad interface of return and >>= .

Je dirais que l'analogie OO la plus proche des monades est le " pattern pattern ".

Dans le modèle de commande, vous enveloppez une instruction ou une expression ordinaire dans un objet de commande . L'objet de commande expose une méthode execute qui exécute l'instruction enveloppée. Ainsi, les déclarations sont transformées en objets de première classe qui peuvent être transmis et exécutés à volonté. Les commandes peuvent être composées afin que vous puissiez créer un objet-programme en enchaînant et en imbriquant des objets de commande.

Les commandes sont exécutées par un objet distinct, l' invocateur . L'avantage de l'utilisation du modèle de commande (plutôt que d'exécuter simplement une série d'instructions ordinaires) est que différents invokers peuvent appliquer une logique différente à la façon dont les commandes doivent être exécutées.

Le modèle de commande peut être utilisé pour ajouter (ou supprimer) des fonctionnalités de langue qui ne sont pas prises en charge par le langage hôte. Par exemple, dans un langage OO hypothétique sans exceptions, vous pouvez ajouter une sémantique d'exception en exposant les méthodes "try" et "throw" aux commandes. Lorsqu'une commande appelle throw, l'invocateur effectue un retour arrière dans la liste (ou l'arborescence) des commandes jusqu'au dernier appel "try". Inversement, vous pouvez supprimer la sémantique d'exception d'une langue (si vous pensez que les exceptions sont mauvaises ) en interceptant toutes les exceptions émises par chaque commande et en les transformant en codes d'erreur qui sont ensuite transmis à la commande suivante.

Des sémantiques d'exécution encore plus fantaisistes comme des transactions, des exécutions non déterministes ou des continuations peuvent être implémentées de la sorte dans un langage qui ne le supporte pas nativement. C'est un modèle assez puissant si vous y réfléchissez.

Maintenant, en réalité, les modèles de commande ne sont pas utilisés comme une caractéristique générale du langage comme celle-ci. L'overhead de transformer chaque déclaration en classe distincte conduirait à une quantité insupportable de code standard. Mais en principe, il peut être utilisé pour résoudre les mêmes problèmes que les monades sont utilisés pour résoudre dans fp.