Peu scientifique sur les monades

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:

 //  - ,     ,   map data class MyRandom<A>( val get: (bits: Int) -> A ) { companion object { val intRandom: MyRandom<Int> = MyRandom { Random.nextBits(it) } val hexRandom: MyRandom<String> = intRandom.map { it.toString(16) } } } //  map   fun <A, B> MyRandom<A>.map(f: (A) -> B): MyRandom<B> = MyRandom(get = {bits -> f(get(bits)) }) fun main(args: Array<String>) { println("random=" + MyRandom.intRandom.get(12)) //  random=1247 println("hexRandom=" + MyRandom.hexRandom.get(12)) //  hexRandom=c25 } 

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> { //   ,  map     - //    flatMap  lift fun <B> map(f: (A) -> B): Monad<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Monad<B>): Monad<B> } fun <A> lift(value: A): Monad<A> = TODO() 

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:

 // ,     Int //       -      //           val readInt: Monad<Int> = TODO() // ,      -  fun readBytes(len: Int): Monad<ByteArray> = TODO() // ,     ,    val bytes: Monad<ByteArray> = readInt.flatMap {len -> if (len > 0) readBytes(len) //    -   else lift(ByteArray(0)) //  ,    } 

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) // 20 println(mul(Option(null), Option(5)).value) // null println(mul(Option(4), Option(null)).value) // null println(mul(Option(null), Option(null)).value) // null } 

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() // .... TODO() } fun update(value: String): DB<Unit> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun selectThenUpdate(id: Int): DB<Unit> = select(id).flatMap { value -> update(value) } fun executeTransaction(c: Connection): Unit { //  ,     //          val body: DB<Unit> = selectThenUpdate(42) //  ,   select  update body.f(c) c.commit() } 

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.io
typelevel.org/cats/typeclasses.html
wiki.haskell.org/All_About_Monads

Mon expérience est une application de style FP à part entière sur Scala:
github.com/scf37/fpscala2

PS Je voulais une petite note, il s'est avéré comme toujours.

Source: https://habr.com/ru/post/fr454534/


All Articles