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:
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> {
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:
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)
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()
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.iotypelevel.org/cats/typeclasses.htmlwiki.haskell.org/All_About_MonadsMeu experimento é um aplicativo de estilo FP completo no Scala:
github.com/scf37/fpscala2PS Eu queria uma pequena nota, acabou como sempre.