Bonjour à tous.
Après quatre ans de programmation à Scala, ma compréhension des monades s'est finalement développée au point où vous pouvez l'expliquer aux autres sans référence à la théorie des catégories et à la
monade classique
- c'est juste un monoïde dans la catégorie de l'endofonction , ce qui n'effraie pas les programmeurs pas plus que les cafards dichlorvos.
Des exemples de code seront écrits en Kotlin, comme il est assez populaire et en même temps assez fonctionnel (dans les deux sens du terme).
Commençons par le concept de
foncteur , le voici:
interface Functor<A>
Quelle est sa signification? Un foncteur est une abstraction d'un calcul arbitraire qui renvoie un résultat de type A. Nous ignorons comment créer un nouveau foncteur et, surtout, comment calculer sa valeur A. En particulier, une fonction peut se cacher derrière une interface de foncteur avec un nombre arbitraire d'arguments, et pas nécessairement une fonction pure.
Exemples d'implémentations de foncteurs:
- constant
- fonction avec un nombre arbitraire d'arguments qui renvoie un résultat de type
A
- générateur pseudo-aléatoire d'état (aléatoire)
- générateur de nombres aléatoires matériel
- lecture d'un objet à partir du disque ou du réseau
- calcul asynchrone - un rappel est passé à l'implémentation du foncteur, qui sera appelé un peu plus tard
Tous ces exemples, à l'exception de la constante, ont une propriété importante - ils sont paresseux, c'est-à-dire le calcul lui-même ne se produit pas lorsque le foncteur est créé, mais lorsqu'il est calculé.
L'interface
Functor<A>
ne permet ni d'obtenir une valeur de type
A
partir de
Functor<A>
, ni de créer un nouveau
Functor<A>
partir d'une valeur existante de type
A
Mais même avec de telles restrictions, le foncteur n'est pas inutile - si pour certains types
B
nous pouvons convertir
A
en
B
(en d'autres termes, il y a une fonction
(a: A) -> B
), alors nous pouvons écrire une fonction
(f: Functor<A>) -> Functor<B>
et nommez-le
map
:
interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> }
Contrairement au foncteur lui-même, la méthode map ne peut pas être une fonction arbitraire:
-
map((a) -> a)
devrait retourner le même foncteur
-
map((a) -> f(a)).map((b) -> g(b))
doit être identique à
map(a -> g(f(a))
Par exemple, nous implémentons un foncteur qui retourne une valeur A contenant un certain nombre de bits aléatoires. Notre interface dans Kotlin ne peut pas être utilisée si facilement (mais vous
pouvez , si vous le souhaitez), nous allons donc écrire une méthode d'extension:
Autres exemples de foncteurs avec une
map
utile
List<A>
immuable List<A>
MyInputStream<A>
Optional<A>
Maintenant, vous pouvez aller dans des monades.
Une monade est un foncteur avec deux opérations supplémentaires. Tout d'abord, la monade, contrairement au foncteur, contient l'opération de création à partir d'une constante, cette opération est appelée
lift
:
fun <A> lift(value: A): Monad<A> = TODO()
La deuxième opération est appelée
flatMap
, elle est plus compliquée, nous allons donc d'abord donner l'intégralité de notre interface monade:
interface Monad<A> {
La différence la plus importante entre une monade et un foncteur est que les monades peuvent être
combinées entre elles, générant de nouvelles monades et abstraite de la façon dont la monade est implémentée - si elle lit à partir du disque, si elle accepte des paramètres supplémentaires pour calculer sa valeur, cette valeur existe-t-elle . Le deuxième point important - les monades ne sont pas combinées en parallèle, mais séquentiellement, laissant la possibilité d'ajouter de la logique en fonction du résultat de la première monade.
Un exemple:
Cependant, dans cet exemple, il n'est pas fait mention d'un réseau. De même, les données peuvent être lues à partir d'un fichier ou d'une base de données. Ils peuvent être lus de manière synchrone ou asynchrone, ici il peut y avoir une gestion des erreurs - tout dépend de l'implémentation spécifique de la monade, le code lui-même restera inchangé.
Au début, l'exemple est plus simple, la monade Option. En kotlin, ce n'est pas vraiment nécessaire, mais en Java / Scala c'est extrêmement utile:
data class Option<A>(val value: A?) { fun <B> map(f: (A) -> B): Option<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Option<B>): Option<B> = when(value) { null -> Option(null) else -> f(value) } } fun <A> lift(value: A?): Option<A> = Option(value) fun mul(a: Option<Int>, b: Option<Int>): Option<Int> = a.flatMap { a -> b.map { b -> a * b } } fun main(args: Array<String>) { println(mul(Option(4), Option(5)).value)
En tant que monade de pozakovyristie, terminons le travail avec la base de données dans la monade:
data class DB<A>(val f: (Connection) -> A) { fun <B> map(f: (A) -> B): DB<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> DB<B>): DB<B> = DB { conn -> f(this.f(conn)).f(conn) } } fun <A> lift(value: A): DB<A> = DB { value } fun select(id: Int): DB<String> = DB { conn -> val st = conn.createStatement()
Le trou du lapin est-il profond?
Il existe une grande variété de monades, mais leur objectif principal est d'abstraire la logique métier de l'application de certains détails des calculs effectués:
- que la valeur peut ne pas exister:
data class Option<A>(value: A?)
- que le calcul échouera:
data class Either<Error, A>(value: Pair<Error?, A?>)
- que le calcul peut être paresseux:
data class Defer<A>(value: () -> A)
- ou asynchrone:
java.util.concurrent.CompletableFuture<A>
- ou avoir un état fonctionnel:
data class State<S, A>(value: (S) -> Pair<S, A>)
Liste des questions sans réponse:
- foncteurs applicatifs - un lien intermédiaire entre les foncteurs et les monades
- des collections comme des monades
- compositions de monades monotypiques - arrow gluesi, transformateurs monadiques
- séquence / cheminement
- monades comme effets
- monades et récursivité, débordement de pile, trampoline
- Encodage final sans étiquette
- Io monad
- et généralement tout le zoo des monades standard
Et ensuite?
arrow-kt.iotypelevel.org/cats/typeclasses.htmlwiki.haskell.org/All_About_MonadsMon expérience est une application de style FP à part entière sur Scala:
github.com/scf37/fpscala2PS Je voulais une petite note, il s'est avéré comme toujours.