Hola a todos
Después de cuatro años de programación en Scala, mi comprensión de las mónadas finalmente ha crecido hasta el punto de que puede explicarlo a otros sin referencia a la teoría de categorías y la
mónada clásica
: es solo un monoide en la categoría de endofunders , que asusta a los programadores no peor que las cucarachas de diclorvos.
Los ejemplos de código se escribirán en Kotlin, como Es bastante popular y, al mismo tiempo, bastante funcional (en ambos sentidos de la palabra).
Comencemos con el concepto de
functor , aquí está:
interface Functor<A>
Cual es su significado Un functor es una abstracción de un cálculo arbitrario que devuelve un resultado de tipo A. Ignoramos cómo crear un nuevo functor y, lo más importante, cómo calcular su valor A. En particular, una función puede esconderse detrás de una interfaz de functor con un número arbitrario de argumentos, y no necesariamente una función pura.
Ejemplos de implementaciones de functor:
- constante
- funciona con un número arbitrario de argumentos que devuelve un resultado de tipo
A
- generador pseudoaleatorio de estado (Aleatorio)
- generador de números aleatorios de hardware
- leer un objeto desde el disco o desde la red
- cálculo asíncrono: se pasa una devolución de llamada a la implementación del functor, que se llamará más adelante
Todos estos ejemplos, excepto la constante, tienen una propiedad importante: son vagos, es decir el cálculo en sí no ocurre cuando se crea el functor, sino cuando se calcula.
La interfaz de functor no permite obtener un valor de tipo
A
del
Functor<A>
o crear un nuevo
Functor<A>
partir de un valor existente de tipo
A
Pero incluso con tales restricciones, el functor no es inútil: si para algún tipo
B
podemos convertir
A
en
B
(en otras palabras, hay una función
(a: A) -> B
), entonces podemos escribir una función
(f: Functor<A>) -> Functor<B>
y
(f: Functor<A>) -> Functor<B>
nombre
map
:
interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> }
A diferencia del functor en sí, el método de mapa no puede ser una función arbitraria:
-
map((a) -> a)
debería devolver el mismo functor
-
map((a) -> f(a)).map((b) -> g(b))
debe ser idéntico al
map(a -> g(f(a))
Como ejemplo, implementamos un functor que devuelve un valor A que contiene un cierto número de bits aleatorios. Nuestra interfaz en Kotlin no se puede usar tan fácilmente (pero
puede hacerlo, si lo desea), por lo que escribiremos un método de extensión:
Otros ejemplos de functores con un
map
útil.
List<A>
inmutable List<A>
MyInputStream<A>
Optional<A>
Ahora puedes ir a las mónadas.
Una mónada es un functor con dos operaciones adicionales. En primer lugar, la mónada, a diferencia del functor, contiene la operación de crear desde una constante, esta operación se llama
lift
:
fun <A> lift(value: A): Monad<A> = TODO()
La segunda operación se llama
flatMap
, es más complicada, así que primero le daremos toda nuestra interfaz de mónada:
interface Monad<A> {
La diferencia más importante entre una mónada y un functor es que las mónadas se pueden
combinar entre sí, generando nuevas mónadas y abstrayendo de cómo se implementa la mónada, ya sea que lea del disco, si acepta parámetros adicionales para calcular su valor, ¿existe este valor? . El segundo punto importante: las mónadas no se combinan en paralelo, sino secuencialmente, dejando la capacidad de agregar lógica dependiendo del resultado de la primera mónada.
Un ejemplo:
Sin embargo, en este ejemplo no se menciona una red. Igualmente bien, los datos se pueden leer desde un archivo o desde una base de datos. Se pueden leer de forma síncrona o asíncrona, aquí puede haber un manejo de errores: todo depende de la implementación específica de la mónada, el código en sí permanecerá sin cambios.
Al principio, el ejemplo es más simple, la opción mónada. En kotlin, no es realmente necesario, pero en Java / Scala es extremadamente ú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 una mónada de pozakovyristy, terminemos el trabajo con la base de datos en la 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()
¿Es profunda la madriguera del conejo?
Hay una gran variedad de mónadas, pero su objetivo principal es abstraer la lógica empresarial de la aplicación a partir de algunos detalles de los cálculos realizados:
- que el valor puede no existir:
data class Option<A>(value: A?)
- que el cálculo fallará:
data class Either<Error, A>(value: Pair<Error?, A?>)
- que el cálculo puede ser vago:
data class Defer<A>(value: () -> A)
- o asíncrono:
java.util.concurrent.CompletableFuture<A>
- o tiene un estado funcional:
data class State<S, A>(value: (S) -> Pair<S, A>)
Lista de preguntas sin respuesta:
- functores aplicativos - un enlace intermedio entre functores y mónadas
- colecciones como mónadas
- composiciones de mónadas monotípicas - arrow gluesi, transformadores monádicos
- secuencia / poligonal
- mónadas como efectos
- mónadas y recursión, desbordamiento de pila, trampolín
- Codificación final sin etiquetas
- Io mónada
- y en general todo el zoológico de mónadas estándar
Que sigue
arrow-kt.iotypelevel.org/cats/typeclasses.htmlwiki.haskell.org/All_About_MonadsMi experimento es una aplicación completa de estilo FP en Scala:
github.com/scf37/fpscala2PD: quería una pequeña nota, resultó como siempre.