Comment identifiez-vous les modèles de conception monadiques?

Je vais apprendre Haskell Je commence à comprendre le concept de monade et commence à utiliser les monades connues dans mon code, mais j’ai toujours des difficultés à aborder les monades du sharepoint vue du concepteur. Dans OO, il y a plusieurs règles comme “identifier les noms” pour les objects, surveiller une sorte d’état et d’interface … mais je ne suis pas capable de trouver des ressources équivalentes pour les monades.

Alors, comment identifiez-vous un problème de nature monadique? Quels sont les bons modèles de conception pour la conception monadique? Quelle est votre approche lorsque vous réalisez qu’un code serait mieux transformé en une monade?

Une règle empirique utile est lorsque vous voyez des valeurs dans un contexte ; les monades peuvent être vues comme des “effets” superposés sur:

  • Peut-être: partialité (utilise des calculs qui peuvent échouer)
  • Soit: erreurs de court-circuit (utilisations: gestion des erreurs / exceptions)
  • [] (la liste monade): non déterminisme (utilisations: génération de liste, filtrage, …)
  • Etat: une seule référence mutable (utilise: état)
  • Reader: un environnement partagé (utilise: liaisons de variables, informations communes, …)
  • Writer: une sortie ou une accumulation “side-channel” (utilise: la journalisation, le maintien d’un compteur en écriture seule, …)
  • Cont: stream de contrôle non local (utilisations: trop nombreuses pour être listées)

Généralement, vous devez généralement concevoir votre monade en la superposant aux transformateurs monade de la bibliothèque Monad Transformer standard, ce qui vous permet de combiner les effets ci-dessus en une seule monade. Ensemble, ils traitent la majorité des monades que vous souhaitez utiliser. Il existe des monades supplémentaires non incluses dans la MTL, telles que les monades de probabilité et de fourniture .

En ce qui concerne le développement d’une intuition pour savoir si un type nouvellement défini est une monade, et comment il se comporte comme tel, vous pouvez y penser en allant de Functor à Monad :

  • Functor vous permet de transformer des valeurs avec des fonctions pures.
  • Applicative vous permet d’intégrer des valeurs pures et des applications express – (<*>) vous permet de passer d’une fonction incorporée et de son argument incorporé à un résultat incorporé.
  • Monad laisse la structure des calculs incorporés dépendre des valeurs des calculs précédents.

La manière la plus simple de comprendre ceci est de regarder le type de join :

 join :: (Monad m) => m (ma) -> ma 

Cela signifie que si vous avez un calcul incorporé dont le résultat est un nouveau calcul intégré, vous pouvez créer un calcul qui exécute le résultat de ce calcul. Vous pouvez donc utiliser des effets monadiques pour créer un nouveau calcul basé sur les valeurs des calculs précédents et transférer le stream de contrôle vers ce calcul.

Fait intéressant, cela peut être une faiblesse pour structurer les choses de manière monadique: avec Applicative , la structure du calcul est statique (un calcul Applicative donné a une certaine structure d’effets qui ne peut pas changer en fonction des valeurs intermédiaires), alors qu’avec Monad il est dynamic. Cela peut limiter l’optimisation que vous pouvez faire; Par exemple, les parsingurs applicatifs sont moins puissants que les parsingurs monadiques (bon, ce n’est pas ssortingctement vrai , mais c’est effectivement le cas), mais ils peuvent être optimisés.

Notez que (>>=) peut être défini comme

 m >>= f = join (fmap fm) 

et donc une monade peut être définie simplement avec return et join (en supposant que c’est un Functor ; toutes les monades sont des foncteurs applicatifs, mais la hiérarchie de classes de Haskell ne l’exige malheureusement pas pour des raisons historiques ).

En outre, vous ne devriez probablement pas trop vous focaliser sur les monades, quel que soit le type de buzz qu’elles suscitent chez les non-Haskellers. Il existe de nombreuses classes de caractères qui représentent des modèles significatifs et puissants, et tout ne se traduit pas forcément par une monade. Applicatif , Monoïde , Pliable … dont l’abstraction à utiliser dépend entièrement de votre situation. Et, bien sûr, le simple fait que quelque chose soit une monade ne signifie pas que cela ne peut pas être autre chose. être une monade est juste une autre propriété d’un type.

Donc, vous ne devriez pas trop penser à “identifier les monades”; les questions ressemblent plus à:

  • Ce code peut-il être exprimé sous une forme monadique plus simple? Avec quelle monade?
  • Est-ce que ce type vient de définir une monade? De quels modèles génériques encodés par les fonctions standard sur les monades puis-je profiter?

Suivez les types.

Si vous trouvez que vous avez des fonctions écrites avec tous ces types

  • (a -> b) -> YourType a -> YourType b
  • a -> YourType a
  • YourType (YourType a) -> YourType a

ou tous ces types

  • a -> YourType a
  • YourType a -> (a -> YourType b) -> YourType b

alors YourType peut être une monade. (Je dis «peut» parce que les fonctions doivent obéir aux lois de la monade aussi.)

(Rappelez-vous que vous pouvez réorganiser les arguments, par exemple YourType a -> (a -> b) -> YourType b est juste (a -> b) -> YourType a -> YourType b déguisé.)

Ne faites pas attention aux monades! Si vous avez des fonctions de tous ces types

  • YourType
  • YourType -> YourType -> YourType

et ils obéissent aux lois du monoïde, vous avez un monoïde! Cela peut aussi être utile. De même pour les autres types de machines, surtout Functor.

Il y a la vue effet des monades:

  • Peut-être – partialité / défaillance en court-circuit
  • Soit – rapport d’erreur / court-circuit (comme Peut-être avec plus d’informations)
  • Writer – n’écrit que “state”, se connectant généralement
  • Lecteur – état en lecture seule, communément à l’environnement
  • Etat – lecture / écriture
  • Reprise – calcul pausable
  • Liste – succès multiples

Une fois que vous êtes familiarisé avec ces effets, il est facile de construire des monades en les combinant avec des transformateurs monad. Notez que la combinaison de certaines monades nécessite des soins particuliers (en particulier des monades avec ou sans retour).

Une chose importante à noter est qu’il n’y a pas beaucoup de monades. Il y a des exotiques qui ne sont pas dans les bibliothèques standard, par exemple la monade de probabilité et les variations de la monade de contours comme Codensity. Mais à moins que vous ne fassiez quelque chose de mathématique, il est peu probable que vous inventiez (ou découvriez) une nouvelle monade, mais si vous utilisez Haskell assez longtemps, vous construirez de nombreuses monades qui sont des combinaisons différentes des standard.

Edit – Notez également que l’ordre dans lequel vous emstackz les transformateurs monad se traduit par des monades différentes:

Si vous ajoutez ErrorT (transformer) à un monareur Writer, vous obtenez cette monade, Either err (log,a) – vous ne pouvez accéder au journal que si vous n’avez pas d’erreur.

Si vous ajoutez WriterT (transfomer) à une monade d’erreur, vous obtenez cette monade (log, Either err a) qui donne toujours access au journal.

C’est une sorte de non-réponse, mais je pense qu’il est important de dire de toute façon. Il suffit de demander! StackOverflow, / r / haskell et le canal irc #haskell sont tous d’excellents endroits pour obtenir des commentaires rapides de personnes intelligentes. Si vous travaillez sur un problème et que vous soupçonnez qu’il existe une magie monadique qui pourrait vous faciliter la tâche, n’hésitez pas! La communauté Haskell aime résoudre les problèmes et est ridiculement amicale.

Ne vous méprenez pas, je ne vous encourage pas à ne jamais apprendre par vous-même. Au contraire, l’interaction avec la communauté Haskell est l’une des meilleures façons d’apprendre. LYAH et RWH , 2 livres Haskell disponibles gratuitement en ligne, sont également fortement recommandés.

Oh, et n’oubliez pas de jouer, jouer, jouer! Lorsque vous jouez avec du code monadique, vous commencez à ressentir ce que les monades “de forme” ont, et quand des combinateurs monadiques peuvent être utiles. Si vous lancez votre propre monade, le système de type vous guidera généralement vers une solution simple et évidente. Mais pour être honnête, vous devriez rarement avoir à lancer votre propre instance de Monad, car les bibliothèques Haskell fournissent des tonnes de choses utiles, comme mentionné par les autres intervenants.

Il y a une notion commune que l’on voit dans de nombreux langages de programmation une “balise de fonction infectieuse” – un comportement spécial pour une fonction qui doit également s’étendre à ses appelants.

  • Les fonctions antirouille peuvent être unsafe , c’est-à-dire qu’elles effectuent des opérations susceptibles de violer la sécurité mémoire. unsafe fonctions unsafe peuvent appeler des fonctions normales, mais toute fonction qui appelle une fonction unsafe doit également être unsafe .
  • Les fonctions Python peuvent être async , ce qui signifie qu’elles renvoient une promesse plutôt qu’une valeur réelle. async fonctions async peuvent appeler des fonctions normales, mais l’appel d’une fonction async (via await ) ne peut être effectué que par une autre fonction async .
  • Les fonctions Haskell peuvent être impures , c’est-à-dire qu’elles renvoient un IO a plutôt qu’un a . Les fonctions impures peuvent appeler des fonctions pures, mais les fonctions impures ne peuvent être appelées que par d’autres fonctions impures.
  • Les fonctions mathématiques peuvent être partielles , ce qui signifie qu’elles ne mappent pas chaque valeur de leur domaine à une sortie. Les définitions des fonctions partielles peuvent faire référence à des fonctions totales, mais si une fonction totale associe une partie de son domaine à une fonction partielle, elle devient également partielle.

Bien qu’il puisse y avoir des façons d’invoquer une fonction balisée à partir d’une fonction non étiquetée, il n’y a pas de manière générale , et cela peut souvent être dangereux et menacer de briser l’abstraction que le langage essaie de fournir.

L’avantage d’avoir des balises est que vous pouvez exposer un ensemble de primitives spéciales auxquelles cette balise est atsortingbuée et faire en sorte que toute fonction utilisant ces primitives la rende claire dans sa signature.

Supposons que vous soyez un concepteur de langage et que vous reconnaissiez ce modèle, et que vous décidiez d’autoriser les balises définies par l’utilisateur. Disons que l’utilisateur a défini une balise Err , représentant des calculs susceptibles de générer une erreur. Une fonction utilisant Err peut ressembler à ceci:

 function div  (n: Int, d: Int): Int if d == 0 throwError("division by 0") else return (n / d) 

Si nous voulions simplifier les choses, nous pourrions observer qu’il n’y a rien d’erreur dans la prise d’arguments – c’est calculer la valeur de retour où des problèmes peuvent survenir. Donc, nous pouvons restreindre les balises aux fonctions qui ne prennent aucun argument, et avoir div retourne une fermeture plutôt que la valeur réelle:

 function div(n: Int, d: Int):  () -> Int () => if d == 0 throwError("division by 0") else return (n / d) 

Dans un langage paresseux tel que Haskell, nous n’avons pas besoin de la fermeture et nous pouvons simplement retourner directement une valeur paresseuse:

 div :: Int -> Int -> Err Int div _ 0 = throwError "division by 0" div nd = return $ n / d 

Il est maintenant évident que, dans Haskell, les balises n’ont pas besoin de prise en charge linguistique particulière – ce sont des constructeurs de type ordinaire. Faisons une classe pour eux!

 class Tag m where 

Nous voulons pouvoir appeler une fonction non étiquetée à partir d’une fonction étiquetée, ce qui équivaut à transformer une valeur non étiquetée ( a ) en une valeur étiquetée ( ma ).

  addTag :: a -> ma 

Nous voulons également pouvoir prendre une valeur étiquetée ( ma ) et appliquer une fonction étiquetée ( a -> mb ) pour obtenir un résultat balisé ( mb ):

  embed :: ma -> (a -> mb) -> mb 

Ceci, bien sûr, est précisément la définition d’une monade! addTag correspond à return et embed correspond à (>>=) .

Il est maintenant clair que les “fonctions marquées” sont simplement un type de monade. En tant que tel, chaque fois que vous repérez un endroit où une “balise de fonction” pourrait s’appliquer, il y a des chances que vous ayez une place adaptée à une monade.

PS En ce qui concerne les tags que j’ai mentionnés dans cette réponse: Haskell modélise l’impureté avec la monade IO et la partialité avec la monade Maybe . La plupart des langages implémentent les promesses / asynchrones de manière assez transparente, et il semble y avoir un paquet Haskell appelé promesse qui imite cette fonctionnalité. La monade Err est équivalente à la monade Either Ssortingng . Je ne suis au courant d’aucun langage qui modélise la mémoire avec une sécurité monadique, cela pourrait être fait.