Opção/Talvez, Qualquer um e Futuro Mônadas em JavaScript, Python, Ruby, Swift e Scala

Publicados: 2022-03-11

Este tutorial de mônadas fornece uma breve explicação sobre mônadas e mostra como implementar as mais úteis em cinco linguagens de programação diferentes - se você estiver procurando por mônadas em JavaScript, mônadas em Python, mônadas em Ruby, mônadas em Swift e/ou mônadas em Scala, ou para comparar qualquer implementação, você está lendo o artigo certo!

Usando essas mônadas, você se livrará de uma série de bugs, como exceções de ponteiro nulo, exceções não tratadas e condições de corrida.

Isto é o que eu abordo abaixo:

  • Uma introdução à teoria das categorias
  • A definição de uma mônada
  • Implementações da Option (“Talvez”) monad, qualquer monad e Future monad, além de um programa de exemplo que as utiliza, em JavaScript, Python, Ruby, Swift e Scala

Vamos começar! Nossa primeira parada é a teoria das categorias, que é a base das mônadas.

Introdução à Teoria das Categorias

A teoria das categorias é um campo matemático que foi desenvolvido ativamente em meados do século XX. Agora é a base de muitos conceitos de programação funcional, incluindo a mônada. Vamos dar uma olhada rápida em alguns conceitos de teoria de categorias, ajustados para a terminologia de desenvolvimento de software.

Portanto, existem três conceitos principais que definem uma categoria:

  1. O tipo é exatamente como o vemos em linguagens de tipagem estática. Exemplos: Int , String , Dog , Cat , etc.
  2. As funções conectam dois tipos. Portanto, eles podem ser representados como uma seta de um tipo para outro tipo ou para si mesmos. A função $f$ do tipo $T$ para o tipo $U$ pode ser denotada como $f: T \to U$. Você pode pensar nisso como uma função de linguagem de programação que recebe um argumento do tipo $T$ e retorna um valor do tipo $U$.
  3. Composição é uma operação, denotada pelo operador $\cdot$, que constrói novas funções a partir das existentes. Em uma categoria, é sempre garantido para qualquer função $f: T \to U$ e $g: U \to V$ existe uma função única $h: T \to V$. Esta função é denotada como $f \cdot g$. A operação mapeia efetivamente um par de funções para outra função. Em linguagens de programação, esta operação é, obviamente, sempre possível. Por exemplo, se você tem uma função que retorna o comprimento de uma string —$strlen: String \to Int$—e uma função que diz se o número é par —$even: Int \to Boolean$—então você pode fazer um function $even{\_}strlen: String \to Boolean$ que informa se o comprimento da String é par. Neste caso $even{\_}strlen = even \cdot strlen$. A composição implica duas características:
    1. Associatividade: $f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
    2. A existência de uma função identidade: $\forall T: \exists f: T \to T$, ou em linguagem simples, para cada tipo $T$ existe uma função que mapeia $T$ para si mesmo.

Então, vamos dar uma olhada em uma categoria simples.

Uma categoria simples envolvendo String, Int e Double, e algumas funções entre elas.

Nota lateral: Estamos assumindo que Int , String e todos os outros tipos aqui são garantidos como não nulos, ou seja, o valor nulo não existe.

Nota 2: Na verdade, isso é apenas parte de uma categoria, mas é tudo o que queremos para nossa discussão, porque tem todas as partes essenciais de que precisamos e o diagrama fica menos confuso dessa maneira. A categoria real também teria todas as funções compostas como $roundToString: Double \to String = intToString \cdot round$, para satisfazer a cláusula de composição de categorias.

Você pode notar que as funções nesta categoria são super simples. Na verdade é quase impossível ter um bug nessas funções. Não há nulos, nem exceções, apenas a aritmética e o trabalho com memória. Portanto, a única coisa ruim que pode acontecer é falha no processador ou na memória - nesse caso, você precisa travar o programa de qualquer maneira - mas isso acontece muito raramente.

Não seria bom se todo o nosso código funcionasse neste nível de estabilidade? Absolutamente! Mas e a E/S, por exemplo? Definitivamente não podemos viver sem ele. Aqui é onde as soluções monad vêm em socorro: elas isolam todas as operações instáveis ​​em pedaços de código superpequenos e muito bem auditados - então você pode usar cálculos estáveis ​​em todo o seu aplicativo!

Digite Mônadas

Vamos chamar o comportamento instável como I/O de efeito colateral . Agora queremos ser capazes de trabalhar com todas as nossas funções previamente definidas como length e tipos como String de forma estável na presença deste efeito colateral .

Então vamos começar com uma categoria vazia $M[A]$ e transformá-la em uma categoria que terá valores com um tipo específico de efeito colateral e também valores sem efeitos colaterais. Vamos supor que definimos esta categoria e ela está vazia. No momento não há nada útil que possamos fazer com ele, então para torná-lo útil, seguiremos estas três etapas:

  1. Preencha-o com valores dos tipos da categoria $A$, como String , Int , Double , etc. (caixas verdes no diagrama abaixo)
  2. Uma vez que temos esses valores, ainda não podemos fazer nada significativo com eles, então precisamos de uma maneira de pegar cada função $f: T \to U$ de $A$ e criar uma função $g: M[T] \to M [U]$ (setas azuis no diagrama abaixo). Uma vez que temos essas funções, podemos fazer tudo com os valores da categoria $M[A]$ que conseguimos fazer na categoria $A$.
  3. Agora que temos uma nova categoria $M[A]$, uma nova classe de funções surge com a assinatura $h: T \to M[U]$ (setas vermelhas no diagrama abaixo). Eles surgem como resultado da promoção de valores na primeira etapa como parte de nossa base de código, ou seja, nós os escrevemos conforme necessário; essas são as principais coisas que diferenciam trabalhar com $M[A]$ versus trabalhar com $A$. O passo final será fazer com que essas funções funcionem bem também em tipos em $M[A]$, ou seja, ser capaz de derivar a função $m:M[T]\to M[U]$ de $h:T\ para M[U]$

Criando uma nova categoria: Categorias A e M[A], mais uma seta vermelha de A's Double para M[A]'s Int, rotulada como "roundAsync". M[A] reutiliza todos os valores e funções de A neste ponto.

Então vamos começar definindo duas maneiras de promover valores de tipos $A$ para valores de tipos $M[A]$: uma função sem efeitos colaterais e outra com efeitos colaterais.

  1. A primeira é chamada $pure$ e é definida para cada valor de uma categoria estável: $pure: T \to M[T]$. Os valores $M[T]$ resultantes não terão nenhum efeito colateral, portanto, essa função é chamada $pure$. Por exemplo, para uma mônada de E/S, $pure$ retornará algum valor imediatamente sem possibilidade de falha.
  2. O segundo é chamado $constructor$ e, ao contrário de $pure$, retorna $M[T]$ com alguns efeitos colaterais. Um exemplo de tal $constructor$ para uma mônada de E/S assíncrona pode ser uma função que busca alguns dados da web e os retorna como uma String . O valor retornado por $constructor$ terá o tipo $M[String]$ neste caso.

Agora que temos duas maneiras de promover valores em $M[A]$, cabe a você como programador escolher qual função usar, dependendo dos objetivos do programa. Vamos considerar um exemplo aqui: Você quer buscar uma página HTML como https://www.toptal.com/javascript/option-maybe-either-future-monads-js e para isso você faz uma função $fetch$. Como qualquer coisa pode dar errado ao buscá-lo - pense em falhas de rede, etc. - você usará $M[String]$ como o tipo de retorno dessa função. Então será algo como $fetch: String \to M[String]$ e em algum lugar no corpo da função vamos usar $constructor$ para $M$.

Agora vamos supor que criamos uma função simulada para teste: $fetchMock: String \to M[String]$. Ele ainda tem a mesma assinatura, mas desta vez apenas injetamos a página HTML resultante dentro do corpo de $fetchMock$ sem fazer nenhuma operação de rede instável. Portanto, neste caso, usamos apenas $pure$ na implementação de $fetchMock$.

Como próximo passo, precisamos de uma função que promova com segurança qualquer função arbitrária $f$ da categoria $A$ para $M[A]$ (setas azuis em um diagrama). Esta função é chamada $map: (T \to U) \to (M[T] \to M[U])$.

Agora temos uma categoria (que pode ter efeitos colaterais se usarmos $constructor$), que também possui todas as funções da categoria estável, o que significa que elas também são estáveis ​​em $M[A]$. Você pode notar que introduzimos explicitamente outra classe de funções como $f: T \to M[U]$. Por exemplo, $pure$ e $construtor$ são exemplos de tais funções para $U = T$, mas obviamente poderia haver mais, como se usássemos $pure$ e depois $map$. Então, geralmente, precisamos de uma maneira de lidar com funções arbitrárias na forma $f: T \to M[U]$.

Se quisermos fazer uma nova função baseada em $f$ que possa ser aplicada a $M[T]$, podemos tentar usar $map$. Mas isso nos levará à função $g: M[T] \to M[M[U]]$, o que não é bom já que não queremos ter mais uma categoria $M[M[A]]$. Para lidar com este problema, introduzimos uma última função: $flatMap: (T \to M[U]) \to (M[T] \to M[U])$.

Mas por que queremos fazer isso? Vamos supor que estamos após o passo 2, ou seja, temos $pure$, $constructor$ e $map$. Digamos que queremos pegar uma página HTML de toptal.com, então escanear todos os URLs lá, bem como buscá-los. Eu faria uma função $fetch: String \to M[String]$ que busca apenas uma URL e retorna uma página HTML.

Então eu aplicaria esta função a uma URL e obteria uma página de toptal.com, que é $x: M[String]$. Agora, faço alguma transformação em $x$ e finalmente chego a alguma URL $u: M[String]$. Eu quero aplicar a função $fetch$ a ela, mas não posso, porque ela recebe o tipo $String$, não $M[String]$. É por isso que precisamos de $flatMap$ para converter $fetch: String \to M[String]$ para $m_fetch: M[String] \to M[String]$.

Agora que completamos todas as três etapas, podemos compor quaisquer transformações de valor que precisarmos. Por exemplo, se você tem o valor $x$ do tipo $M[T]$ e $f: T \to U$, você pode usar $map$ para aplicar $f$ ao valor $x$ e obter o valor $y$ do tipo $M[U]$. Dessa forma, qualquer transformação de valores pode ser feita de maneira 100% livre de bugs, desde que as implementações $pure$, $constructor$, $map$ e $flatMap$ estejam livres de bugs.

Então, em vez de lidar com alguns efeitos desagradáveis ​​toda vez que você os encontrar em sua base de código, você só precisa ter certeza de que apenas essas quatro funções estão implementadas corretamente. No final do programa, você obterá apenas um $M[X]$ onde poderá desempacotar com segurança o valor $X$ e lidar com todos os casos de erro.

Isso é o que é uma mônada: uma coisa que implementa $pure$, $map$ e $flatMap$. (Na verdade, $map$ pode ser derivado de $pure$ e $flatMap$, mas é uma função muito útil e difundida, então não a omiti da definição.)

A Option Monad, também conhecida como Maybe Monad

Ok, vamos mergulhar na implementação prática e uso de mônadas. A primeira mônada realmente útil é a mônada da Opção. Se você vem de linguagens de programação clássicas, provavelmente já encontrou muitas falhas por causa do infame erro de ponteiro nulo. Tony Hoare, o inventor do null, chama essa invenção de “The Billion Dollar Mistake”:

Isso levou a inúmeros erros, vulnerabilidades e falhas no sistema, que provavelmente causaram um bilhão de dólares de dor e danos nos últimos quarenta anos.

Então vamos tentar melhorar isso. A Option monad possui algum valor não nulo ou nenhum valor. Bastante semelhante a um valor nulo, mas com essa mônada, podemos usar com segurança nossas funções bem definidas sem ter medo da exceção do ponteiro nulo. Vamos dar uma olhada nas implementações em diferentes linguagens:

JavaScript—Opção Mônada/Talvez 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—Opção Mônada/Talvez 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()

Ruby—Opção Mônada/Talvez 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—Opção Mônada/Talvez 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—Opção Mônada/Talvez 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]

Começamos implementando uma classe Monad que será a base para todas as nossas implementações monad. Ter essa classe é muito útil, porque implementando apenas dois de seus métodos - pure e flatMap - para uma mônada específica, você obterá muitos métodos de graça (nós os limitamos simplesmente ao método map em nossos exemplos, mas geralmente há muitos métodos outros métodos úteis, como sequence e traverse para trabalhar com matrizes de Monad ).

Podemos expressar map como composição de pure e flatMap . Você pode ver na assinatura do flatMap $flatMap: (T \to M[U]) \to (M[T] \to M[U])$ que está muito próximo de $map: (T \to U) \ para (M[T] \para M[U])$. A diferença é o $M$ adicional no meio, mas podemos usar a função pure para converter $U$ em $M[U]$. Dessa forma, expressamos map em termos de flatMap e pure .

Isso funciona bem para o Scala, porque possui um sistema de tipos avançado. Também funciona bem para JS, Python e Ruby, porque são tipados dinamicamente. Infelizmente, isso não funciona para Swift, porque é digitado estaticamente e não possui recursos de tipo avançados, como tipos de tipo superior, então para Swift teremos que implementar map para cada mônada.

Observe também que a Option monad já é um padrão de fato para linguagens como Swift e Scala, então usamos nomes ligeiramente diferentes para nossas implementações de monad.

Agora que temos uma classe base Monad , vamos para nossas implementações de Option monad. Como mencionado anteriormente, a ideia básica é que Option ou possui algum valor (chamado Some ) ou ou não possui nenhum valor ( None ).

O método pure simplesmente promove um valor para Some , enquanto o método flatMap verifica o valor atual da Option — se for None , ele retorna None , e se for Some com um valor subjacente , extrai o valor subjacente, aplica f() a e retorna um resultado.

Observe que apenas usando essas duas funções e map , é impossível entrar em uma exceção de ponteiro nulo – nunca. (O problema pode surgir em nossa implementação do método flatMap , mas são apenas algumas linhas em nosso código que verificamos uma vez. Depois disso, apenas usamos nossa implementação Option monad em todo o nosso código em milhares de lugares e não tem que temer a exceção do ponteiro nulo.)

A Qualquer Mônada

Vamos mergulhar na segunda mônada: qualquer um. Isso é basicamente o mesmo que a mônada Option, mas com Some chamado Right e None chamado Left . Mas desta vez, a Left também pode ter um valor subjacente.

Precisamos disso porque é muito conveniente expressar o lançamento de uma exceção. Se ocorrer uma exceção, o valor de Either será Left(Exception) . A função flatMap não progride se o valor for Left , que repete a semântica de lançar exceções: Se uma exceção aconteceu, paramos a execução.

JavaScript—Qualquer Mônada

 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 — Qualquer Mônada

 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

Ruby — Qualquer uma das 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 — Qualquer Mônada

 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 — Qualquer Mônada

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

Observe também que é fácil capturar exceções: Tudo o que você precisa fazer é mapear Left to Right . (Embora não façamos isso em nossos exemplos, por brevidade.)

A Mônada do Futuro

Vamos explorar a última mônada que precisamos: a Mônada do Futuro. A Mônada do Futuro é basicamente um contêiner para um valor que está disponível agora ou estará disponível em um futuro próximo. Você pode fazer cadeias de Futures com map e flatMap que esperarão que o valor Future seja resolvido antes de executar o próximo pedaço de código que depende do valor que está sendo resolvido primeiro. Isso é muito semelhante ao conceito de Promises em JS.

Nosso objetivo de design agora é unir as APIs assíncronas existentes em diferentes idiomas para uma base consistente. Acontece que a abordagem de design mais fácil é usar callbacks em $constructor$.

Embora o design do callback tenha introduzido o problema do callback hell em JavaScript e outras linguagens, não será um problema para nós, já que usamos mônadas. Na verdade, o objeto Promise – a base para a solução do JavaScript para callback hell – é uma mônada em si!

E o construtor da Mônada do Futuro? Tem esta assinatura:

constructor :: ((Either err a -> void) -> void) -> Future (Either err a)

Vamos dividi-lo em pedaços. Primeiro, vamos definir:

type Callback = Either err a -> void

Portanto, Callback é uma função que recebe um erro ou um valor resolvido como argumento e não retorna nada. Agora nossa assinatura fica assim:

constructor :: (Callback -> void) -> Future (Either err a)

Portanto, precisamos fornecer uma função que não retorne nada e acione um retorno de chamada assim que a computação assíncrona for resolvida para um erro ou algum valor. Parece fácil o suficiente para fazer uma ponte para qualquer idioma.

Quanto ao design da mônada do Futuro, vejamos sua estrutura interna. A ideia chave é ter uma variável de cache que mantenha um valor se a mônada do Futuro for resolvida, ou não retenha nada de outra forma. Você pode se inscrever no Future com um callback que será acionado imediatamente se o valor for resolvido, ou se não, colocará o callback na lista de assinantes.

Depois que o Future for resolvido, cada callback nesta lista será acionado exatamente uma vez com o valor resolvido em um thread separado (ou como a próxima função a ser executada no loop de eventos, no caso de JS.) Observe que é crucial use cuidadosamente as primitivas de sincronização, caso contrário, as condições de corrida são possíveis.

O fluxo básico é: você inicia a computação assíncrona fornecida como o argumento do construtor e aponta seu retorno de chamada para nosso método de retorno de chamada interno. Enquanto isso, você pode se inscrever no Future monad e colocar seus callbacks na fila. Uma vez que o cálculo é feito, o método de retorno de chamada interno chama todos os retornos de chamada na fila. Se você estiver familiarizado com extensões reativas (RxJS, RxSwift, etc.), elas usam uma abordagem muito semelhante ao tratamento assíncrono.

A API pública do Future monad consiste em pure , map e flatMap , assim como nas mônadas anteriores. Também precisaremos de alguns métodos úteis:

  1. async , que pega uma função de bloqueio síncrona e a executa em um thread separado, e
  2. traverse , que recebe um array de valores e uma função que mapeia um valor para um Future e retorna um Future de um array de valores resolvidos

Vamos ver como isso se desenrola:

JavaScript—Futura Mônada

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

Ruby—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) }) }) }

Agora, observe como a API pública do Future não contém detalhes de baixo nível, como threads, semáforos ou qualquer coisa desse tipo. Tudo que você precisa é basicamente fornecer algo com um retorno de chamada, e pronto!

Compondo um programa de Monads

Ok, então vamos tentar usar nossas mônadas para fazer um programa real. Digamos que temos um arquivo com uma lista de URLs e queremos buscar cada uma dessas URLs em paralelo. Em seguida, queremos cortar as respostas para 200 bytes cada para brevidade e imprimir o resultado.

Começamos convertendo as APIs de linguagem existentes para interfaces monádicas (veja as funções readFile e fetch ). Agora que temos isso, podemos apenas compô-los para obter o resultado final como uma cadeia. Observe que a própria corrente é super segura, pois todos os detalhes sangrentos estão contidos em mônadas.

JavaScript—Exemplo de Programa Monad

 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! Impressionante!