Opción/Tal vez, Cualquiera y Future Monads en JavaScript, Python, Ruby, Swift y Scala
Publicado: 2022-03-11Este tutorial de mónadas brinda una breve explicación de las mónadas y muestra cómo implementar las más útiles en cinco lenguajes de programación diferentes, si está buscando mónadas en JavaScript, mónadas en Python, mónadas en Ruby, mónadas en Swift y/o mónadas en Scala, o para comparar cualquier implementación, ¡está leyendo el artículo correcto!
Al usar estas mónadas, se librará de una serie de errores, como excepciones de puntero nulo, excepciones no controladas y condiciones de carrera.
Esto es lo que cubro a continuación:
- Una introducción a la teoría de categorías.
- La definición de una mónada.
- Implementaciones de la mónada Opción ("Quizás"), la mónada Cualquiera y la mónada Futuro, además de un programa de muestra que las aprovecha, en JavaScript, Python, Ruby, Swift y Scala
¡Empecemos! Nuestra primera parada es la teoría de categorías, que es la base de las mónadas.
Introducción a la teoría de categorías
La teoría de categorías es un campo matemático que se desarrolló activamente a mediados del siglo XX. Ahora es la base de muchos conceptos de programación funcional, incluida la mónada. Echemos un vistazo rápido a algunos conceptos de teoría de categorías, adaptados a la terminología de desarrollo de software.
Así que hay tres conceptos básicos que definen una categoría:
- El tipo es tal como lo vemos en los lenguajes tipificados estáticamente. Ejemplos:
Int
,String
,Dog
,Cat
, etc. - Las funciones conectan dos tipos. Por tanto, pueden representarse como una flecha de un tipo a otro tipo, o hacia sí mismos. La función $f$ del tipo $T$ al tipo $U$ se puede denotar como $f: T \to U$. Puede considerarlo como una función de lenguaje de programación que toma un argumento de tipo $T$ y devuelve un valor de tipo $U$.
- La composición es una operación, denotada por el operador $\cdot$, que construye nuevas funciones a partir de las existentes. En una categoría, siempre se garantiza que para cualquier función $f: T \to U$ y $g: U \to V$ existe una única función $h: T \to V$. Esta función se denota como $f \cdot g$. La operación asigna efectivamente un par de funciones a otra función. En lenguajes de programación, esta operación es, por supuesto, siempre posible. Por ejemplo, si tiene una función que devuelve la longitud de una cadena —$strlen: String \to Int$—y una función que indica si el número es par —$even: Int \to Boolean$—entonces puede hacer una function $even{\_}strlen: String \to Boolean$ que indica si la longitud de
String
es par. En este caso $par{\_}strlen = par \cdot strlen$. La composición implica dos características:- Asociatividad: $f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
- La existencia de una función de identidad: $\forall T: \exists f: T \to T$, o en lenguaje sencillo, para cada tipo $T$ existe una función que asigna $T$ a sí misma.
Así que echemos un vistazo a una categoría simple.
Nota al margen: asumimos que Int
, String
y todos los demás tipos aquí están garantizados como no nulos, es decir, el valor nulo no existe.
Nota al margen 2: en realidad, esto es solo una parte de una categoría, pero eso es todo lo que queremos para nuestra discusión, porque tiene todas las partes esenciales que necesitamos y el diagrama está menos abarrotado de esta manera. La categoría real también tendría todas las funciones compuestas como $roundToString: Double \to String = intToString \cdot round$, para satisfacer la cláusula de composición de categorías.
Puede notar que las funciones en esta categoría son súper simples. De hecho, es casi imposible tener un error en estas funciones. No hay nulos, ni excepciones, solo la aritmética y el trabajo con la memoria. Entonces, lo único malo que puede suceder es una falla en el procesador o en la memoria, en cuyo caso debe bloquear el programa de todos modos, pero eso sucede muy rara vez.
¿No sería bueno que todo nuestro código funcionara con este nivel de estabilidad? ¡Absolutamente! Pero, ¿qué pasa con las E/S, por ejemplo? Definitivamente no podemos vivir sin él. Aquí es donde las soluciones de mónadas vienen al rescate: aíslan todas las operaciones inestables en piezas de código súper pequeñas y muy bien auditadas, ¡entonces puede usar cálculos estables en toda su aplicación!
Entrar Mónadas
Llamemos a un comportamiento inestable como E/S un efecto secundario . Ahora queremos poder trabajar con todas nuestras funciones previamente definidas como length
y tipos como String
de manera estable en presencia de este efecto secundario .
Entonces, comencemos con una categoría vacía $M[A]$ y convirtámosla en una categoría que tendrá valores con un tipo particular de efecto secundario y también valores sin efectos secundarios. Supongamos que hemos definido esta categoría y está vacía. Ahora mismo no hay nada útil que podamos hacer con él, así que para que sea útil, seguiremos estos tres pasos:
- Rellénelo con valores de los tipos de la categoría $A$, como
String
,Int
,Double
, etc. (cuadros verdes en el diagrama a continuación) - Una vez que tenemos estos valores, todavía no podemos hacer nada significativo con ellos, por lo que necesitamos una forma de tomar cada función $f: T \to U$ de $A$ y crear una función $g: M[T] \to M [U]$ (flechas azules en el siguiente diagrama). Una vez que tenemos estas funciones, podemos hacer todo con los valores en la categoría $M[A]$ que pudimos hacer en la categoría $A$.
- Ahora que tenemos una nueva categoría $M[A]$, emerge una nueva clase de funciones con la firma $h: T \to M[U]$ (flechas rojas en el diagrama a continuación). Surgen como resultado de promover valores en el paso uno como parte de nuestro código base, es decir, los escribimos según sea necesario; estas son las cosas principales que diferenciarán trabajar con $M[A]$ versus trabajar con $A$. El paso final será hacer que estas funciones también funcionen bien en tipos en $M[A]$, es decir, poder derivar la función $m: M[T] \to M[U]$ de $h: T \ a M[U]$
Entonces, comencemos definiendo dos formas de promover valores de tipos $A$ a valores de tipos $M[A]$: una función sin efectos secundarios y otra con efectos secundarios.
- El primero se llama $puro$ y se define para cada valor de una categoría estable: $puro: T \to M[T]$. Los valores $M[T]$ resultantes no tendrán ningún efecto secundario, por lo que esta función se llama $pura$. Por ejemplo, para una mónada de E/S, $pure$ devolverá algún valor inmediatamente sin posibilidad de falla.
- El segundo se llama $constructor$ y, a diferencia de $pure$, devuelve $M[T]$ con algunos efectos secundarios. Un ejemplo de un $constructor$ de este tipo para una mónada de E/S asíncrona podría ser una función que obtenga algunos datos de la web y los devuelva como una
String
. El valor devuelto por $constructor$ tendrá tipo $M[String]$ en este caso.
Ahora que tenemos dos formas de promover valores en $M[A]$, depende de usted como programador elegir qué función usar, según los objetivos de su programa. Consideremos un ejemplo aquí: desea obtener una página HTML como https://www.toptal.com/javascript/option-maybe-either-future-monads-js y para esto crea una función $fetch$. Dado que cualquier cosa podría salir mal al buscarlo (piense en fallas de la red, etc.), usará $M[String]$ como el tipo de retorno de esta función. Entonces se verá algo como $fetch: String \to M[String]$ y en algún lugar del cuerpo de la función usaremos $constructor$ para $M$.
Ahora supongamos que hacemos una función simulada para probar: $fetchMock: String \to M[String]$. Todavía tiene la misma firma, pero esta vez solo inyectamos la página HTML resultante dentro del cuerpo de $fetchMock$ sin realizar ninguna operación de red inestable. Entonces, en este caso, solo usamos $pure$ en la implementación de $fetchMock$.
Como siguiente paso, necesitamos una función que promueva de manera segura cualquier función arbitraria $f$ de la categoría $A$ a $M[A]$ (flechas azules en un diagrama). Esta función se llama $map: (T \to U) \to (M[T] \to M[U])$.
Ahora tenemos una categoría (que puede tener efectos secundarios si usamos $constructor$), que también tiene todas las funciones de la categoría estable, lo que significa que también son estables en $M[A]$. Puede notar que introdujimos explícitamente otra clase de funciones como $f: T \to M[U]$. Por ejemplo, $pure$ y $constructor$ son ejemplos de tales funciones para $U = T$, pero obviamente podría haber más, como si usáramos $pure$ y luego $map$. Entonces, generalmente, necesitamos una forma de manejar funciones arbitrarias en la forma $f: T \to M[U]$.
Si queremos hacer una nueva función basada en $f$ que pueda aplicarse a $M[T]$, podríamos intentar usar $map$. Pero eso nos llevará a la función $g: M[T] \to M[M[U]]$, lo cual no es bueno ya que no queremos tener una categoría más $M[M[A]]$. Para solucionar este problema, introducimos una última función: $flatMap: (T \to M[U]) \to (M[T] \to M[U])$.
Pero, ¿por qué querríamos hacer eso? Supongamos que buscamos el paso 2, es decir, tenemos $pure$, $constructor$ y $map$. Digamos que queremos obtener una página HTML de toptal.com, luego escanear todas las URL allí y buscarlas. Haría una función $fetch: String \to M[String]$ que obtenga solo una URL y devuelva una página HTML.
Luego, aplicaría esta función a una URL y obtendría una página de toptal.com, que es $x: M[String]$. Ahora, realizo una transformación en $x$ y finalmente llego a una URL $u: M[String]$. Quiero aplicarle la función $fetch$, pero no puedo, porque toma el tipo $String$, no $M[String]$. Es por eso que necesitamos $flatMap$ para convertir $fetch: String \to M[String]$ a $m_fetch: M[String] \to M[String]$.
Ahora que hemos completado los tres pasos, podemos componer cualquier transformación de valor que necesitemos. Por ejemplo, si tiene un valor $x$ de tipo $M[T]$ y $f: T \to U$, puede usar $map$ para aplicar $f$ al valor $x$ y obtener el valor $y$ de tipo $M[U]$. De esa manera, cualquier transformación de valores se puede realizar 100 por ciento libre de errores, siempre que las implementaciones de $pure$, $constructor$, $map$ y $flatMap$ estén libres de errores.
Entonces, en lugar de lidiar con algunos efectos desagradables cada vez que los encuentre en su base de código, solo necesita asegurarse de que solo estas cuatro funciones se implementen correctamente. Al final del programa, obtendrá solo un $M[X]$ donde puede desenvolver con seguridad el valor $X$ y manejar todos los casos de error.
Esto es lo que es una mónada: algo que implementa $pure$, $map$ y $flatMap$. (En realidad, $map$ se puede derivar de $pure$ y $flatMap$, pero es una función muy útil y generalizada, por lo que no la omití de la definición).
La Mónada Opción, también conocida como la Mónada Quizá
Bien, profundicemos en la implementación práctica y el uso de las mónadas. La primera mónada realmente útil es la mónada Opción. Si viene de los lenguajes de programación clásicos, probablemente haya encontrado muchos bloqueos debido al infame error del puntero nulo. Tony Hoare, el inventor de nulo, llama a este invento "El error del billón de dólares":
Esto ha llevado a innumerables errores, vulnerabilidades y bloqueos del sistema, que probablemente han causado mil millones de dólares en dolor y daños en los últimos cuarenta años.
Así que tratemos de mejorar eso. La mónada Opción tiene algún valor no nulo o ningún valor. Bastante similar a un valor nulo, pero con esta mónada, podemos usar con seguridad nuestras funciones bien definidas sin tener miedo de la excepción del puntero nulo. Echemos un vistazo a las implementaciones en diferentes idiomas:
JavaScript—Opción Mónada/Tal vez Mónada
class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }
Python—Opción Mónada/Tal vez Mónada
class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()
Rubí—Opción Mónada/Tal vez Mónada
class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()
Swift—Opción Mónada/Tal vez Mónada
import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }
Scala—Opción Mónada/Tal vez Mónada
import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]
Comenzamos implementando una clase Monad
que será la base para todas nuestras implementaciones de mónadas. Tener esta clase es muy útil, porque al implementar solo dos de sus métodos, pure
y flatMap
, para una mónada específica, obtendrá muchos métodos de forma gratuita (los limitamos simplemente al método de map
en nuestros ejemplos, pero generalmente hay muchos otros métodos útiles, como sequence
y traverse
para trabajar con matrices de Monad
s).
Podemos expresar map
como composición de pure
y flatMap
. Puede ver en la firma de flatMap
$flatMap: (T \to M[U]) \to (M[T] \to M[U])$ que está muy cerca de $map: (T \to U) \ a (M[T] \a M[U])$. La diferencia son los $M$ adicionales en el medio, pero podemos usar la función pure
para convertir $U$ en $M[U]$. De esa forma expresamos map
en términos de flatMap
y pure
.
Esto funciona bien para Scala, porque tiene un sistema de tipo avanzado. También funciona bien para JS, Python y Ruby, porque se escriben dinámicamente. Desafortunadamente, no funciona para Swift, porque está tipado estáticamente y no tiene funciones de tipo avanzado como tipos de tipo superior, por lo que para Swift tendremos que implementar un map
para cada mónada.
También tenga en cuenta que la mónada Option ya es un estándar de facto para lenguajes como Swift y Scala, por lo que usamos nombres ligeramente diferentes para nuestras implementaciones de mónadas.
Ahora que tenemos una clase de Monad
base, vayamos a nuestras implementaciones de mónada de opción. Como se mencionó anteriormente, la idea básica es que Option tiene algún valor (llamado Some
) o no tiene ningún valor ( None
).
El método pure
simplemente promueve un valor a Some
, mientras que el método flatMap
verifica el valor actual de la Option
: si es None
, devuelve None
, y si es Some
con un valor subyacente, extrae el valor subyacente, aplica f()
a y devuelve un resultado.
Tenga en cuenta que solo usando estas dos funciones y map
, es imposible entrar en una excepción de puntero nulo, nunca. (El problema podría surgir potencialmente en nuestra implementación del método flatMap
, pero eso es solo un par de líneas en nuestro código que verificamos una vez. Después de eso, solo usamos nuestra implementación de mónada de opción en todo nuestro código en miles de lugares y no hay que temer la excepción del puntero nulo en absoluto.)
La mónada cualquiera
Sumerjámonos en la segunda mónada: Cualquiera. Esto es básicamente lo mismo que la mónada Option, pero con Some
llamado Right
y None
llamado Left
. Pero esta vez, Left
también puede tener un valor subyacente.
Necesitamos eso porque es muy conveniente expresar lanzar una excepción. Si se produjo una excepción, el valor de Either
será Left(Exception)
. La función flatMap
no progresa si el valor es Left
, que repite la semántica de lanzar excepciones: si ocurre una excepción, detenemos la ejecución.
JavaScript: cualquiera de las dos mónadas
import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure
Python: cualquiera de las dos mónadas
from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False
Rubí: cualquiera de las dos mónadas
require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end
Swift: cualquiera de las dos mónadas
import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }
Scala: cualquiera de las mónadas
package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }
También tenga en cuenta que es fácil detectar excepciones: todo lo que tiene que hacer es mapear de Left
a Right
. (Aunque no lo hacemos en nuestros ejemplos, por brevedad).

La futura mónada
Exploremos la última mónada que necesitamos: la mónada futura. La mónada Future es básicamente un contenedor de un valor que está disponible ahora o estará disponible en un futuro próximo. Puede crear cadenas de Futuros con map
y flatMap
que esperarán a que se resuelva el valor Futuro antes de ejecutar el siguiente fragmento de código que depende del valor que se resuelva primero. Esto es muy similar al concepto de Promises en JS.
Nuestro objetivo de diseño ahora es conectar las API asíncronas existentes en diferentes idiomas a una base consistente. Resulta que el enfoque de diseño más fácil es usar devoluciones de llamada en $constructor$.
Si bien el diseño de devolución de llamada introdujo el problema del infierno de devolución de llamada en JavaScript y otros lenguajes, no será un problema para nosotros, ya que usamos mónadas. De hecho, el objeto Promise
, la base de la solución de JavaScript para el infierno de devolución de llamada, ¡es una mónada en sí misma!
¿Qué pasa con el constructor de la mónada del Futuro? Tiene esta firma:
constructor :: ((Either err a -> void) -> void) -> Future (Either err a)
Vamos a dividirlo en pedazos. Primero, definamos:
type Callback = Either err a -> void
Entonces Callback
es una función que toma un error o un valor resuelto como argumento y no devuelve nada. Ahora nuestra firma se ve así:
constructor :: (Callback -> void) -> Future (Either err a)
Por lo tanto, debemos proporcionarle una función que no devuelva nada y active una devolución de llamada tan pronto como el cálculo asíncrono se resuelva en un error o en algún valor. Parece bastante fácil hacer un puente para cualquier idioma.
En cuanto al diseño de la propia mónada Futuro, veamos su estructura interna. La idea clave es tener una variable de caché que contenga un valor si la mónada Future se resuelve, o no contenga nada en caso contrario. Puede suscribirse a Future con una devolución de llamada que se activará inmediatamente si se resuelve el valor o, si no, colocará la devolución de llamada en la lista de suscriptores.
Una vez que se resuelve el futuro, cada devolución de llamada en esta lista se activará exactamente una vez con el valor resuelto en un subproceso separado (o como la siguiente función que se ejecutará en el ciclo de eventos, en el caso de JS). Tenga en cuenta que es crucial use con cuidado las primitivas de sincronización, de lo contrario, las condiciones de carrera son posibles.
El flujo básico es: inicia el cálculo asíncrono proporcionado como argumento del constructor y apunta su devolución de llamada a nuestro método interno de devolución de llamada. Mientras tanto, puede suscribirse a la mónada Future y poner sus devoluciones de llamadas en la cola. Una vez que se realiza el cálculo, el método de devolución de llamada interno llama a todas las devoluciones de llamada en la cola. Si está familiarizado con las extensiones reactivas (RxJS, RxSwift, etc.), utilizan un enfoque muy similar para el manejo asincrónico.
La API pública de la mónada Future consta de pure
, map
y flatMap
, al igual que en las mónadas anteriores. También necesitaremos un par de métodos prácticos:
-
async
, que toma una función de bloqueo síncrono y la ejecuta en un subproceso separado, y -
traverse
, que toma una matriz de valores y una función que asigna un valor a unFuture
y devuelve unFuture
de una matriz de valores resueltos
Veamos cómo se desarrolla eso:
JavaScript: mónada futura
import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )
Python: futura mónada
from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()
Rubí: futura mónada
require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end
Swift: futura mónada
import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }
Scala: futura mónada
package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }
Ahora, observe cómo la API pública de Future
no contiene detalles de bajo nivel como hilos, semáforos o nada de eso. Todo lo que necesita es básicamente proporcionar algo con una devolución de llamada, ¡y eso es todo!
Componer un programa a partir de mónadas
Bien, intentemos usar nuestras mónadas para hacer un programa real. Digamos que tenemos un archivo con una lista de URL y queremos obtener cada una de estas URL en paralelo. Luego, queremos reducir las respuestas a 200 bytes cada una por brevedad e imprimir el resultado.
Comenzamos convirtiendo las API de idioma existentes en interfaces monádicas (consulte las funciones readFile
y fetch
). Ahora que tenemos eso, podemos componerlos para obtener el resultado final como una cadena. Tenga en cuenta que la cadena en sí es súper segura, ya que todos los detalles sangrientos están contenidos en mónadas.
JavaScript—Programa Monad de muestra
import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)
Python—Sample Monad Program
import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()
Ruby—Sample Monad Program
require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop
Swift—Sample Monad Program
import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()
Scala—Sample Monad Program
import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }
There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.
Overhead: Done. Benefits: Ongoing
For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! ¡Increíble!