Não científico sobre mônadas

Olá pessoal.

Após quatro anos de programação em Scala, meu conhecimento sobre mônadas finalmente chegou ao ponto em que você pode explicá-lo a outras pessoas sem referência à teoria de categorias e à mônada clássica - é apenas um monóide na categoria de endofunção , que afugenta os programadores tanto quanto as baratas de diclorvos.

Exemplos de código serão escritos em Kotlin, como é bastante popular e, ao mesmo tempo, bastante funcional (nos dois sentidos da palavra).

Vamos começar com o conceito de um functor , aqui está:

interface Functor<A> 

Qual é o seu significado? Um functor é uma abstração de uma computação arbitrária que retorna um resultado do tipo A. Ignoramos como criar um novo functor e, o mais importante, como calcular seu valor A. Em particular, uma função pode se esconder atrás de uma interface de functor com um número arbitrário de argumentos, e não necessariamente uma função pura.

Exemplos de implementações de functor:

  • constante
  • função com um número arbitrário de argumentos que retorna um resultado do tipo A
  • gerador pseudo-aleatório do estado (aleatório)
  • gerador de números aleatórios de hardware
  • lendo um objeto do disco ou da rede
  • cálculo assíncrono - um retorno de chamada é passado para a implementação do functor, que será chamado algum tempo depois

Todos esses exemplos, exceto a constante, têm uma propriedade importante - são preguiçosos, ou seja, o próprio cálculo não ocorre quando o functor é criado, mas quando é calculado.

A interface do functor não permite obter um valor do tipo A do Functor<A> ou criar um novo Functor<A> partir de um valor existente do tipo A Mas mesmo com essas restrições, o functor não é inútil - se, para algum tipo B , podemos converter A em B (em outras palavras, existe uma função (a: A) -> B ), então podemos escrever uma função (f: Functor<A>) -> Functor<B> e nomeie o map :

 interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> } 

Ao contrário do próprio functor, o método map não pode ser uma função arbitrária:
- o map((a) -> a) deve retornar o mesmo functor
- map((a) -> f(a)).map((b) -> g(b)) deve ser idêntico ao map(a -> g(f(a))

Como exemplo, implementamos um functor que retorna um valor A que contém um certo número de bits aleatórios. Nossa interface no Kotlin não pode ser usada com tanta facilidade (mas você pode , se desejar), portanto, escreveremos um método de extensão:

 //  - ,     ,   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 } 

Outros exemplos de functores com um map útil

  • List<A> imutável List<A>
  • MyInputStream<A>
  • Optional<A>

Agora você pode ir às mônadas.

Uma mônada é um functor com duas operações adicionais. Antes de tudo, a mônada, ao contrário do functor, contém a operação de criação a partir de uma constante, essa operação é chamada de lift :

 fun <A> lift(value: A): Monad<A> = TODO() 

A segunda operação é chamada flatMap , é mais complicada; portanto, primeiro forneceremos toda a interface da mônada:

 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() 

A diferença mais importante entre uma mônada e um functor é que as mônadas podem ser combinadas entre si, gerando novas mônadas e abstraindo-se de como a mônada é implementada - se ela lê do disco, se aceita parâmetros adicionais para calcular seu valor, esse valor existe? . O segundo ponto importante - as mônadas não são combinadas em paralelo, mas sequencialmente, deixando a capacidade de adicionar lógica, dependendo do resultado da primeira mônada.

Um exemplo:

 // ,     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)) //  ,    } 

No entanto, neste exemplo, não há menção de uma rede. Igualmente bem, os dados podem ser lidos de um arquivo ou de um banco de dados. Eles podem ser lidos de forma síncrona ou assíncrona, aqui pode haver tratamento de erros - tudo depende da implementação específica da mônada, o código em si permanecerá inalterado.

A princípio, o exemplo é mais simples, a mônada Option. No kotlin, isso não é realmente necessário, mas no Java / Scala é extremamente útil:

 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 } 

Como mônada de pozakovyristy, vamos encerrar o trabalho com o banco de dados na mônada:

 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() } 

A toca do coelho é profunda?


Há uma enorme variedade de mônadas, mas seu principal objetivo é abstrair a lógica comercial do aplicativo a partir de alguns detalhes dos cálculos realizados:

  • que o valor pode não existir: data class Option<A>(value: A?)
  • que o cálculo falhará: data class Either<Error, A>(value: Pair<Error?, A?>)
  • que o cálculo pode ser preguiçoso: data class Defer<A>(value: () -> A)
  • ou assíncrono: java.util.concurrent.CompletableFuture<A>
  • ou possui um estado funcional: data class State<S, A>(value: (S) -> Pair<S, A>)

Lista de perguntas não respondidas:

  • functors aplicativos - um link intermediário entre functors e mônadas
  • coleções como mônadas
  • composições de mônadas monotípicas - arrow gluesi, transformadores monádicos
  • sequência / travessia
  • mônadas como efeitos
  • mônadas e recursão, excesso de pilha, trampolim
  • Codificação final sem etiqueta
  • Io monad
  • e geralmente todo o zoológico de mônadas padrão

O que vem a seguir?


arrow-kt.io
typelevel.org/cats/typeclasses.html
wiki.haskell.org/All_About_Monads

Meu experimento é um aplicativo de estilo FP completo no Scala:
github.com/scf37/fpscala2

PS Eu queria uma pequena nota, acabou como sempre.

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


All Articles