Mônada em inglês simples? (Para o programador OOP sem experiência em FP)

743

Em termos que um programador de OOP entenderia (sem nenhum fundo de programação funcional), o que é uma mônada?

Que problema ele resolve e quais são os locais mais comuns em que é usado?

EDITAR:

Para esclarecer o tipo de entendimento que eu estava procurando, digamos que você estava convertendo um aplicativo FP que tinha mônadas em um aplicativo OOP. O que você faria para portar as responsabilidades das mônadas para o aplicativo OOP?


fonte
10
@Pavel: A resposta que recebemos de Eric abaixo é muito melhor do que a dos outros Qs sugeridos para pessoas com experiência em OO (ao contrário de experiência em FP).
Donal Fellows
5
@Donal: Se este é um idiota (sobre o qual não tenho opinião), a boa resposta deve ser adicionada ao original. Ou seja: uma boa resposta não impede o fechamento como duplicado. Se for uma duplicata suficientemente próxima, isso pode ser feito por um moderador como uma mesclagem.
dmckee --- ex-moderador gatinho

Respostas:

732

ATUALIZAÇÃO: Esta questão foi objeto de uma série de blogs imensamente longa, que você pode ler em Monads - obrigado pela ótima pergunta!

Em termos que um programador de OOP entenderia (sem nenhum fundo de programação funcional), o que é uma mônada?

Uma mônada é um "amplificador" de tipos que obedece a certas regras e que possui determinadas operações .

Primeiro, o que é um "amplificador de tipos"? Por isso, quero dizer algum sistema que permite que você escolha um tipo e o transforme em um tipo mais especial. Por exemplo, em C # considere Nullable<T>. Este é um amplificador de tipos. Ele permite que você escolha um tipo, digamos int, e adicione um novo recurso a esse tipo, a saber, que agora ele pode ser nulo quando não podia antes.

Como segundo exemplo, considere IEnumerable<T> . É um amplificador de tipos. Permite que você escolha um tipo, digamos,string e adicione um novo recurso a esse tipo, ou seja, agora você pode criar uma sequência de cadeias de caracteres a partir de qualquer número de cadeias únicas.

Quais são as "certas regras"? Resumidamente, existe uma maneira sensata de as funções do tipo subjacente trabalharem no tipo amplificado, de modo que sigam as regras normais da composição funcional. Por exemplo, se você tem uma função com números inteiros, diga

int M(int x) { return x + N(x * 2); }

então a função correspondente em Nullable<int> poderá fazer com que todos os operadores e chamadas funcionem juntos "da mesma maneira" que antes.

(Isso é incrivelmente vago e impreciso; você pediu uma explicação que não assumisse nada sobre o conhecimento da composição funcional.)

Quais são as "operações"?

  1. Existe uma operação de "unidade" (confusa, às vezes chamada de operação "de retorno") que pega um valor de um tipo simples e cria o valor monádico equivalente. Isso, em essência, fornece uma maneira de pegar um valor de um tipo não amplificado e transformá-lo em um valor do tipo amplificado. Pode ser implementado como um construtor em uma linguagem OO.

  2. Há uma operação "bind" que recebe um valor monádico e uma função que pode transformar o valor e retorna um novo valor monádico. Bind é a operação principal que define a semântica da mônada. Permite transformar operações no tipo não amplificado em operações no tipo amplificado, que obedecem às regras de composição funcional mencionadas anteriormente.

  3. Geralmente, existe uma maneira de recuperar o tipo não amplificado do tipo amplificado. Estritamente falando, esta operação não é obrigada a ter uma mônada. (Embora seja necessário se você deseja uma comonada . Não consideraremos isso mais adiante neste artigo.)

Mais uma vez, tome Nullable<T>como exemplo. Você pode transformar um intem um Nullable<int>com o construtor. O compilador C # cuida da maioria dos "anulamentos" anuláveis ​​para você, mas, se não o fez, a transformação do levantamento é direta: uma operação, por exemplo,

int M(int x) { whatever }

é transformado em

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

E transformar as Nullable<int>costas em uma inté feito com oValue propriedade

É a transformação da função que é o bit principal. Observe como a semântica real da operação anulável - que uma operação em um nullpropaga onull - é capturada na transformação. Podemos generalizar isso.

Suponha que você tenha uma função de intpara int, como nosso original M. Você pode facilmente transformar isso em uma função que recebe inte retorna a Nullable<int>porque você pode simplesmente executar o resultado através do construtor anulável. Agora, suponha que você tenha este método de ordem superior:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

Veja o que você pode fazer com isso? Qualquer método que recebe inte retorna um int, ou recebe um inte retorna a Nullable<int>agora pode ter a semântica anulável aplicada a ele .

Além disso: suponha que você tenha dois métodos

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

e você quer compor eles:

Nullable<int> Z(int s) { return X(Y(s)); }

Ou seja, Zé a composição de Xe Y. Mas você não pode fazer isso porque Xpega um inte Yretorna um Nullable<int>. Mas como você tem a operação "bind", pode fazer este trabalho:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

A operação de ligação em uma mônada é o que faz a composição de funções em tipos amplificados funcionar. As "regras" acima mencionadas são de que a mônada preserva as regras da composição normal das funções; que a composição com funções de identidade resulta na função original, que a composição é associativa e assim por diante.

Em C #, "Vincular" é chamado "SelectMany". Veja como ele funciona na mônada de sequência. Precisamos ter duas coisas: transformar um valor em uma sequência e vincular operações em seqüências. Como bônus, também temos "transformar uma sequência novamente em um valor". Essas operações são:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

A regra da mônada anulável era "combinar duas funções que produzem nulidades juntas, verifique se a interna resulta em nulo; se isso ocorrer, produza nulo, se não resultar, então chame a externa com o resultado". Essa é a semântica desejada de anulável.

A regra da mônada de sequência é "combinar duas funções que produzem seqüências juntas, aplicar a função externa a todos os elementos produzidos pela função interna e concatenar todas as seqüências resultantes juntas". A semântica fundamental das mônadas é capturada nos métodos Bind/ SelectMany; este é o método que diz o que a mônada realmente significa .

Nós podemos fazer ainda melhor. Suponha que você tenha uma sequência de ints e um método que receba ints e resulte em sequências de strings. Poderíamos generalizar a operação de ligação para permitir a composição de funções que recebem e retornam tipos amplificados diferentes, desde que as entradas de uma correspondam às saídas da outra:

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

Então agora podemos dizer "amplifique esse monte de números inteiros individuais em uma sequência de números inteiros. Transforme esse número inteiro específico em um monte de cadeias, amplificado em uma sequência de cadeias. Agora monte as duas operações: amplifique esse grupo de números inteiros na concatenação de números inteiros. todas as sequências de cordas ". Mônadas permitem compor suas amplificações.

Que problema ele resolve e quais são os locais mais comuns em que é usado?

É como perguntar "que problemas o padrão singleton resolve?", Mas vou tentar.

Mônadas são normalmente usadas para resolver problemas como:

  • Preciso criar novos recursos para esse tipo e ainda combinar funções antigas nesse tipo para usar os novos recursos.
  • Preciso capturar um monte de operações em tipos e representá-las como objetos composíveis, construindo composições cada vez maiores até ter a série correta de operações representada e, em seguida, preciso começar a obter resultados.
  • Preciso representar operações de efeito colateral de maneira limpa em um idioma que odeia efeitos colaterais

C # usa mônadas em seu design. Como já mencionado, o padrão anulável é altamente semelhante ao "talvez mônada". O LINQ é inteiramente construído a partir de mônadas; o SelectManymétodo é o que faz o trabalho semântico da composição das operações. (Erik Meijer gosta de salientar que todas as funções do LINQ poderiam realmente ser implementadas por SelectMany; todo o resto é apenas uma conveniência.)

Para esclarecer o tipo de entendimento que eu estava procurando, digamos que você estava convertendo um aplicativo FP que tinha mônadas em um aplicativo OOP. O que você faria para portar as responsabilidades das mônadas no aplicativo OOP?

A maioria dos idiomas OOP não possui um sistema de tipos suficientemente rico para representar o próprio padrão de mônada diretamente; você precisa de um sistema de tipos que suporte tipos mais altos que tipos genéricos. Então, eu não tentaria fazer isso. Em vez disso, eu implementaria tipos genéricos que representam cada mônada e implementaria métodos que representam as três operações necessárias: transformar um valor em um valor amplificado, (talvez) transformar um valor amplificado em um valor e transformar uma função em valores não amplificados em uma função em valores amplificados.

Um bom lugar para começar é como implementamos o LINQ em C #. Estude o SelectManymétodo; é a chave para entender como a mônada de sequência funciona em c #. É um método muito simples, mas muito poderoso!


Sugestão, leitura adicional:

  1. Para uma explicação mais aprofundada e teoricamente correta das mônadas em C #, recomendo o artigo de Wes Dyer, colega de Eric Lippert , sobre o assunto. Este artigo é o que explicou as mônadas para mim quando elas finalmente "clicaram" para mim.
  2. Uma boa ilustração do motivo pelo qual você pode querer uma mônada (usa Haskell nos exemplos) .
  3. Tipo de "tradução" do artigo anterior para JavaScript.

Eric Lippert
fonte
17
Esta é uma ótima resposta, mas minha cabeça ficou sem graça. Vou acompanhar e encará-lo neste fim de semana e fazer perguntas se as coisas não se acalmarem e fizerem sentido na minha cabeça.
Paul Nathan
5
Excelente explicação, como de costume Eric. Para uma discussão mais teórica (mas ainda muito interessante), achei o post de Bart De Smet no MinLINQ útil para relacionar algumas construções de programação funcional de volta ao C # também. community.bartdesmet.net/blogs/bart/archive/2010/01/01/…
Ron Warholic
41
Faz mais sentido para mim dizer que aumenta os tipos do que os amplifica .
Gabe
61
@ Slomojo: e mudei de volta para o que escrevi e pretendia escrever. Se você e Gabe querem escrever sua própria resposta, vá em frente.
Eric Lippert
24
@ Eric, é você quem decide, mas o amplificador implica que as propriedades existentes sejam aprimoradas, o que é enganoso.
Ocodo 19/10/10
341

Por que precisamos de mônadas?

  1. Queremos programar apenas usando funções . ("programação funcional", afinal de contas -FP).
  2. Então, nós temos um primeiro grande problema. Este é um programa:

    f(x) = 2 * x

    g(x,y) = x / y

    Como podemos dizer o que deve ser executado primeiro ? Como podemos formar uma sequência ordenada de funções (isto é, um programa ) usando não mais que funções?

    Solução: compor funções . Se você quiser primeiro ge depois f, basta escrever f(g(x,y)). OK mas ...

  3. Mais problemas: algumas funções podem falhar (ou seja g(2,0), dividir por 0). Não temos "exceções" no FP . Como resolvemos isso?

    Solução: vamos permitir que as funções retornem dois tipos de coisas : em vez de terg : Real,Real -> Real (função de dois reais em um real), vamos permitir g : Real,Real -> Real | Nothing(função de dois reais em (real ou nada)).

  4. Mas as funções devem (para ser mais simples) retornar apenas uma coisa .

    Solução: vamos criar um novo tipo de dados a serem retornados, um " tipo de boxe " que encerra talvez um real ou seja simplesmente nada. Por isso, podemos ter g : Real,Real -> Maybe Real. OK mas ...

  5. O que acontece agora f(g(x,y))? fnão está pronto para consumir umMaybe Real . E não queremos alterar todas as funções com as quais podemos nos conectar gpara consumir umMaybe Real .

    Solução: vamos ter uma função especial para "conectar" / "compor" / "vincular" funções . Dessa forma, podemos, nos bastidores, adaptar a saída de uma função para alimentar a seguinte.

    No nosso caso: g >>= f(conectar / compor gpara f). Queremos >>=obter ga produção, inspecioná-la e, no caso de Nothingsimplesmente não ligar fe retornar Nothing; ou, pelo contrário, extraia a caixa Reale alimente fcom ela. (Este algoritmo é apenas a implementação de >>=para oMaybe tipo).

  6. Surgem muitos outros problemas que podem ser resolvidos usando esse mesmo padrão: 1. Use uma "caixa" para codificar / armazenar diferentes significados / valores e tenha funções como gessa que retornam esses "valores em caixa". 2. Tenha compositores / vinculadores g >>= fpara ajudar a conectar ga saída àf entrada, para que não tenhamos que mudar f.

  7. Problemas notáveis ​​que podem ser resolvidos usando esta técnica são:

    • tendo um estado global que todas as funções na sequência de funções ("o programa") podem compartilhar: solução StateMonad.

    • Não gostamos de "funções impuras": funções que produzem resultados diferentes para a mesma entrada. Portanto, vamos marcar essas funções, fazendo com que elas retornem um valor marcado / em caixa: IOmônada.

Felicidade total !!!!

cibercitizen1
fonte
2
@DmitriZaitsev Exceções só podem ocorrer em "código impuro" (a mônada de E / S) até onde eu sei.
Cibercitizen1
3
@DmitriZaitsev O papel do Nothing pode ser desempenhado por qualquer outro tipo (diferente do Real esperado). Esse não é o ponto. No exemplo, o problema é como adaptar funções em uma cadeia quando a anterior pode retornar um tipo de valor inesperado para a seguinte, sem encadear a última (apenas aceitando um Real como entrada).
Cibercitizen1
3
Outro ponto de confusão é que a palavra "mônada" aparece apenas duas vezes na sua resposta e apenas em combinação com outros termos - Statee IO, com nenhum deles, bem como com o significado exato de "mônada"
Dmitri Zaitsev
31
Para mim, como uma pessoa oriunda de OOP, essa resposta realmente explicava bem a motivação por trás de uma mônada e também o que ela realmente é (muito mais que uma resposta aceita). Então, acho isso muito útil. Muito obrigado @ cibercitizen1 e +1
akhilless
3
Eu tenho lido e lido sobre programação funcional há cerca de um ano. Essa resposta, e especialmente os dois primeiros pontos, finalmente me fizeram entender o que realmente significa programação imperativa e por que a programação funcional é diferente. Obrigado!
jrahhali
82

Eu diria que a analogia OO mais próxima das mônadas é o " padrão de comando ".

No padrão de comando, você quebra uma declaração ou expressão comum em um objeto de comando . O objeto de comando expõe um método de execução que executa a instrução agrupada. Assim, a instrução é transformada em objetos de primeira classe que podem ser passados ​​e executados à vontade. Os comandos podem ser compostos para que você possa criar um objeto de programa encadeando e aninhando objetos de comando.

Os comandos são executados por um objeto separado, o invocador . O benefício de usar o padrão de comando (em vez de apenas executar uma série de instruções comuns) é que diferentes invocadores podem aplicar lógica diferente à maneira como os comandos devem ser executados.

O padrão de comando pode ser usado para adicionar (ou remover) recursos de idioma que não são suportados pelo idioma do host. Por exemplo, em uma linguagem OO hipotética sem exceções, você pode adicionar semânticas de exceção expondo os métodos "try" e "throw" aos comandos. Quando um comando chama throw, o invocador retorna pela lista (ou árvore) de comandos até a última chamada "try". Por outro lado, você pode remover a semântica de exceção de um idioma (se você acredita que as exceções são ruins ) capturando todas as exceções lançadas por cada comando individual e transformando-as em códigos de erro que são passados ​​para o próximo comando.

Uma semântica de execução ainda mais sofisticada, como transações, execução não determinística ou continuações, pode ser implementada dessa maneira em uma linguagem que não a suporta nativamente. É um padrão bastante poderoso se você pensar sobre isso.

Agora, na realidade, os padrões de comando não são usados ​​como um recurso geral da linguagem como este. A sobrecarga de transformar cada instrução em uma classe separada levaria a uma quantidade insuportável de código clichê. Mas, em princípio, pode ser usado para resolver os mesmos problemas que as mônadas são usadas para resolver em fp.

JacquesB
fonte
15
Acredito que esta seja a primeira explicação da mônada que vi que não se baseia em conceitos de programação funcional e a coloca em termos reais de OOP. Resposta realmente boa.
David K. Hess
isso é muito próximo 2 do que as mônadas realmente são em FP / Haskell, exceto que o próprio objeto de objetos "sabe" a qual "lógica de invocação" pertence (e somente as compatíveis podem ser encadeadas); invoker apenas fornece o primeiro valor. Não é como se o comando "Imprimir" pudesse ser executado por "lógica de execução não determinística". Não, deve ser "lógica de E / S" (isto é, mônada de E / S). Mas fora isso, é muito próximo. Você pode até dizer que as mônadas são apenas programas (construídas com instruções de código, para serem executadas posteriormente). Nos primeiros dias, o "vínculo" era mencionado como "ponto e vírgula programável" .
Will Ness
1
@ DavidK.Hess Na verdade, sou incrivelmente cético em relação às respostas que usam FP para explicar conceitos básicos de FP, e especialmente respostas que usam uma linguagem FP como Scala. Muito bem, JacquesB!
Reintegrar Monica
62

Em termos que um programador de OOP entenderia (sem nenhum fundo de programação funcional), o que é uma mônada?

Que problema ele resolve e quais são os locais mais comuns em que são usados? São os locais mais comuns em que é usado?

Em termos de programação OO, uma mônada é uma interface (ou mais provavelmente uma mixin), parametrizada por um tipo, com dois métodos, returne bindque descrevem:

  • Como injetar um valor para obter um valor monádico desse tipo de valor injetado;
  • Como usar uma função que cria um valor monádico a partir de um valor não monádico, em um valor monádico.

O problema que ele resolve é o mesmo tipo de problema que você esperaria de qualquer interface, a saber: "Eu tenho várias classes diferentes que fazem coisas diferentes, mas parecem fazer essas coisas diferentes de uma maneira que tem uma semelhança subjacente. posso descrever essa semelhança entre eles, mesmo que as próprias classes não sejam realmente subtipos de algo mais próximo do que a própria classe 'Object'? "

Mais especificamente, a Monad"interface" é semelhante IEnumeratorou IIteratorrequer um tipo que por si só. O principal "ponto" do Monadpensamento é poder conectar operações com base no tipo de interior, até ao ponto de ter um novo "tipo interno", mantendo - ou até aprimorando - a estrutura de informações da classe principal.

BMeph
fonte
1
returnna verdade, não seria um método na mônada, porque não leva uma instância de mônada como argumento. (ie: não existe isso / auto) #
Laurence Gonsalves
@LaurenceGonsalves: Como atualmente estou analisando isso para a tese de meu bacharelado, acho que o que limita principalmente é a falta de métodos estáticos nas interfaces em C # / Java. Você poderia percorrer um longo caminho na implementação de toda a história da mônada, pelo menos estaticamente vinculada em vez de baseada em classes de tipo. Curiosamente, isso funcionaria, apesar da falta de tipos mais elevados.
Sebastian Graf
42

Você tem uma apresentação recente " Monadologie - ajuda profissional sobre ansiedade de tipo ", da Christopher League (12 de julho de 2010), que é bastante interessante sobre tópicos de continuação e mônada.
O vídeo que acompanha esta apresentação (slideshare) está realmente disponível no vimeo .
A parte Monad começa em torno de 37 minutos neste vídeo de uma hora e começa com o slide 42 da sua apresentação de 58 slides.

É apresentado como "o principal padrão de design para programação funcional", mas a linguagem usada nos exemplos é Scala, que é OOP e funcional.
Você pode ler mais sobre o Monad em Scala na postagem do blog " Monads - Outra maneira de abstrair os cálculos em Scala ", de Debasish Ghosh (27 de março de 2008).

Um construtor de tipo M é uma mônada se suportar estas operações:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

Então, por exemplo (em Scala):

  • Option é uma mônada
    def unidade [A] (x: A): Opção [A] = Alguns (x)

    def flatMap [A, B] (m: Opção [A]) (f: A => Opção [B]): Opção [B] =
      m corresponde a {
       caso Nenhum => Nenhum
       case Algum (x) => f (x)
      }
  • List é Mônada
    def unidade [A] (x: A): Lista [A] = Lista (x)

    def flatMap [A, B] (m: Lista [A]) (f: A => Lista [B]): Lista [B] =
      m corresponde a {
        caso Nil => Nil
        (x) = f (x) ::: flatMap (xs) (f)
      }

A Mônada é muito importante em Scala por causa da sintaxe conveniente criada para aproveitar as estruturas da Mônada:

forcompreensão em Scala :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

é traduzido pelo compilador para:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

A abstração da chave é a flatMap, que liga a computação através do encadeamento.
Cada chamada de flatMapretorna o mesmo tipo de estrutura de dados (mas de valor diferente), que serve como entrada para o próximo comando da cadeia.

No snippet acima, flatMap recebe como entrada um fechamento (SomeType) => List[AnotherType]e retorna a List[AnotherType]. O ponto importante a ser observado é que todos os flatMaps usam o mesmo tipo de fechamento que a entrada e retornam o mesmo tipo que a saída.

É isso que "liga" o encadeamento da computação - todo item da sequência na compreensão precisa respeitar esta mesma restrição de tipo.


Se você realizar duas operações (que podem falhar) e passar o resultado para a terceira, como:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

mas sem tirar vantagem do Monad, você obtém código OOP complicado, como:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

enquanto que com o Monad, você pode trabalhar com os tipos reais ( Venue, User) como todas as operações e manter oculto o material de verificação da opção, tudo por causa dos mapas planos da sintaxe for:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

A parte de rendimento só será executada se todas as três funções tiverem Some[X]; qualquer um Noneseria retornado diretamente para confirm.


Assim:

As mônadas permitem a computação ordenada dentro da Programação Funcional, que nos permite modelar o seqüenciamento de ações em uma boa forma estruturada, um pouco como uma DSL.

E o maior poder vem da capacidade de compor mônadas que servem a propósitos diferentes, em abstrações extensíveis dentro de um aplicativo.

Esse seqüenciamento e encadeamento de ações por uma mônada é feito pelo compilador de linguagem que faz a transformação através da mágica dos fechamentos.


A propósito, o Monad não é apenas o modelo de computação usado no FP:

A teoria das categorias propõe muitos modelos de computação. Entre eles

  • o modelo de computação Arrow
  • o modelo de computação da Mônada
  • o modelo aplicável de cálculos
VonC
fonte
2
Eu amo essa explicação! O exemplo que você deu demonstra o conceito lindamente e também adiciona o que o IMHO estava faltando no teaser de Eric sobre o SelectMany () ser uma mônada. Thx por isso!
aoven
1
IMHO esta é a resposta mais elegante
Polymerase
e antes de tudo, Functor.
Will Ness
34

Para respeitar os leitores rápidos, começo primeiro por uma definição precisa, continuo com uma explicação mais rápida e simples em inglês e depois passo para exemplos.

Aqui está uma definição concisa e precisa, ligeiramente reformulada:

Uma mônada (em ciência da computação) é formalmente um mapa que:

  • envia todo tipo Xde alguma linguagem de programação para um novo tipo T(X)(chamado "tipo de T-computações com valores em X");

  • equipado com uma regra para compor duas funções da forma f:X->T(Y)e g:Y->T(Z)para uma função g∘f:X->T(Z);

  • de uma maneira associativa no sentido evidente e unital em relação a uma determinada função unitária chamada pure_X:X->T(X), a ser considerada como tendo um valor para a computação pura que simplesmente retorna esse valor.

Portanto, em palavras simples, uma mônada é uma regra para passar de qualquer tipo Xpara outro tipoT(X) , e uma regra para passar de duas funções f:X->T(Y)e g:Y->T(Z)(que você gostaria de compor, mas não pode) para uma nova funçãoh:X->T(Z) . Que, no entanto, não é a composição em sentido matemático estrito. Estamos basicamente "dobrando" a composição da função ou redefinindo como as funções são compostas.

Além disso, exigimos que a regra de composição da mônada satisfaça os axiomas matemáticos "óbvios":

  • Associatividade : Compor fcom ge depois com h(de fora) deve ser o mesmo que compor gcom he depois com f(de dentro).
  • Propriedade unital : a composição fda função de identidade de ambos os lados deve render f.

Novamente, em palavras simples, não podemos simplesmente enlouquecer redefinindo nossa composição de funções como gostamos:

  • Primeiro, precisamos da associatividade para poder compor várias funções em uma linha f(g(h(k(x))), por exemplo , e não nos preocupar em especificar os pares de funções de composição da ordem. Como a regra da mônada apenas prescreve como compor um par de funções , sem esse axioma, precisaríamos saber qual par é composto primeiro e assim por diante. (Observe que é diferente da propriedade de comutatividade que fcompôs com gera a mesma que gcompôs com f, o que não é necessário).
  • E segundo, precisamos da propriedade unital, que é simplesmente dizer que as identidades compõem trivialmente a maneira como as esperamos. Assim, podemos refatorar funções com segurança sempre que essas identidades puderem ser extraídas.

Então, novamente em resumo: uma mônada é a regra das funções de extensão e composição de tipos que satisfazem os dois axiomas - associatividade e propriedade unital.

Em termos práticos, você deseja que a mônada seja implementada para você pelo idioma, compilador ou estrutura que cuidaria das funções de composição para você. Portanto, você pode se concentrar em escrever a lógica de sua função, em vez de se preocupar em como a execução deles é implementada.

É basicamente isso, em poucas palavras.


Sendo matemático profissional, prefiro evitar chamar ha "composição" de fe g. Porque matematicamente, não é. Chamá-lo de "composição" pressupõe incorretamente que hé a verdadeira composição matemática, o que não é. Nem sequer é determinado exclusivamente por fe g. Em vez disso, é o resultado da nova "regra de composição" de nossa mônada. O que pode ser totalmente diferente da composição matemática real, mesmo que exista!


Para torná-lo menos seco, deixe-me tentar ilustrá-lo pelo exemplo que estou anotando em seções pequenas, para que você possa pular direto ao ponto.

Exceção lançada como exemplos da Mônada

Suponha que desejemos compor duas funções:

f: x -> 1 / x
g: y -> 2 * y

Mas f(0)não está definido, portanto, uma exceção eé lançada. Então, como você pode definir o valor composicional g(f(0))? Lance uma exceção novamente, é claro! Talvez o mesmo e. Talvez uma nova exceção atualizada e1.

O que exatamente acontece aqui? Primeiro, precisamos de novos valores de exceção (diferentes ou iguais). Você pode chamá-los nothingou nullqualquer outra coisa, mas a essência permanece a mesma - eles devem ser novos valores, por exemplo, não deve ser um numberem nosso exemplo aqui. Prefiro não ligar para eles nullpara evitar confusão com como nullpode ser implementado em qualquer idioma específico. Da mesma forma, prefiro evitar, nothingporque muitas vezes está associado a null, o que, em princípio, é o que nulldeve fazer; no entanto, esse princípio geralmente é desviado por quaisquer razões práticas.

O que é exceção exatamente?

Este é um assunto trivial para qualquer programador experiente, mas gostaria de soltar algumas palavras apenas para extinguir qualquer worm de confusão:

Exceção é um objeto que encapsula informações sobre como ocorreu o resultado inválido da execução.

Isso pode variar desde jogar fora todos os detalhes e retornar um único valor global (como NaNou null) ou gerar uma longa lista de logs ou o que exatamente aconteceu, enviá-lo para um banco de dados e replicar por toda a camada de armazenamento de dados distribuídos;)

A diferença importante entre esses dois exemplos extremos de exceção é que, no primeiro caso, não há efeitos colaterais . No segundo existem. O que nos leva à questão (de mil dólares):

As exceções são permitidas em funções puras?

Resposta mais curta : Sim, mas apenas quando eles não causam efeitos colaterais.

Resposta mais longa. Para ser pura, a saída da sua função deve ser determinada exclusivamente por sua entrada. Portanto, alteramos nossa função fenviando 0para o novo valor abstrato eque chamamos de exceção. Garantimos que o valor enão contenha informações externas que não sejam determinadas exclusivamente por nossa entrada, o que é x. Então, aqui está um exemplo de exceção sem efeito colateral:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

E aqui está um com efeito colateral:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

Na verdade, só tem efeitos colaterais se essa mensagem puder mudar no futuro. Mas se é garantido que nunca mude, esse valor se tornará previsível de forma única e, portanto, não haverá efeito colateral.

Para torná-lo ainda mais idiota. Uma função que retorna 42sempre é claramente pura. Mas se alguém louco decide criar 42uma variável com esse valor alterado, a mesma função deixa de ser pura sob as novas condições.

Observe que estou usando a notação literal de objeto por simplicidade para demonstrar a essência. Infelizmente, as coisas estão bagunçadas em linguagens como JavaScript, onde errornão é um tipo que se comporta da maneira que queremos aqui em relação à composição de funções, enquanto tipos reais gostam nullou NaNnão se comportam dessa maneira, mas passam por algumas artificiais e nem sempre intuitivas digitar conversões.

Extensão de tipo

Como queremos variar a mensagem dentro de nossa exceção, estamos realmente declarando um novo tipo Epara todo o objeto de exceção e, então, é isso que maybe numberacontece, além de seu nome confuso, que deve ser do tipo numberou do novo tipo de exceção E, então é realmente a união number | Ede numbere E. Em particular, depende de como queremos construir E, o que não é sugerido nem refletido no nome maybe number.

O que é composição funcional?

É os matemáticos funções operação de tomada f: X -> Ye g: Y -> Ze construção de sua composição como função h: X -> Zsatisfazendo h(x) = g(f(x)). O problema com esta definição ocorre quando o resultado f(x)não é permitido como argumento de g.

Em matemática, essas funções não podem ser compostas sem trabalho extra. A solução estritamente matemática para o exemplo acima de fe gé remover 0do conjunto de definições de f. Com esse novo conjunto de definições (novo tipo mais restritivo de x), ftorna-se passível de composição g.

No entanto, não é muito prático em programação restringir o conjunto de definições desse ftipo. Em vez disso, exceções podem ser usadas.

Ou como uma outra abordagem, os valores artificiais são criados como NaN, undefined, null, Infinityetc. Então você avaliar 1/0a Infinitye 1/-0a -Infinity. E, em seguida, force o novo valor de volta à sua expressão, em vez de lançar a exceção. Levando a resultados que você pode ou não achar previsível:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

E estamos de volta aos números regulares prontos para seguir em frente;)

O JavaScript nos permite continuar executando expressões numéricas a qualquer custo, sem gerar erros, como no exemplo acima. Isso significa que também permite compor funções. Qual é exatamente o objetivo da mônada - é uma regra compor funções que satisfaçam os axiomas, conforme definido no início desta resposta.

Mas a regra da função de composição, decorrente da implementação do JavaScript para lidar com erros numéricos, é uma mônada?

Para responder a essa pergunta, tudo que você precisa é verificar os axiomas (deixados como exercício como parte da pergunta aqui;).

A exceção de lançamento pode ser usada para construir uma mônada?

De fato, uma mônada mais útil seria a regra que prescreve que, se flança exceção para alguns x, o mesmo ocorre com sua composição g. Além disso, torne a exceção Eglobalmente única, com apenas um valor possível ( objeto terminal na teoria das categorias). Agora, os dois axiomas são verificáveis ​​instantaneamente e temos uma mônada muito útil. E o resultado é o que é conhecido como a mônada talvez .

Dmitri Zaitsev
fonte
3
Boa contribuição. +1 Mas talvez você queira excluir "encontrou a maioria das explicações por muito tempo ...", sendo a sua a mais longa. Outros julgarão se é "inglês simples", conforme necessário, a pergunta: "inglês simples == em palavras simples, de maneira simples".
Cibercitizen1
@ cibercitizen1 Obrigado! Na verdade, é curto, se você não contar o exemplo. O ponto principal é que você não precisa ler o exemplo para entender a definição . Infelizmente, muitas explicações me obrigam a ler exemplos primeiro , o que muitas vezes é desnecessário, mas, é claro, pode exigir trabalho extra para o escritor. Com muita dependência de exemplos específicos, existe o perigo de que detalhes sem importância ocultem a imagem e dificultem a compreensão. Dito isto, você tem pontos válidos, consulte a atualização.
Dmitri Zaitsev
2
muito longo e confuso
seenimurugan
1
@seenimurugan Sugestões de melhoria são bem-vindas;)
Dmitri Zaitsev
26

Uma mônada é um tipo de dados que encapsula um valor e ao qual, essencialmente, duas operações podem ser aplicadas:

  • return x cria um valor do tipo de mônada que encapsula x
  • m >>= f(leia-o como "o operador de ligação") aplica a função fao valor na mônadam

É isso que uma mônada é. Existem mais alguns detalhes técnicos , mas basicamente essas duas operações definem uma mônada. A verdadeira questão é: "O que uma mônada faz ?", E isso depende das listas de mônadas, mônadas, Maybes são mônadas, operações de IO são mônadas. Tudo o que isso significa quando dizemos que essas coisas são mônadas é que elas têm a interface de mônada de returne >>=.

Mandril
fonte
“O que uma mônada faz, e isso depende da mônada”: e mais precisamente, isso depende da bindfunção que deve ser definida para cada tipo monádico, não é? Essa seria uma boa razão para não confundir ligação com composição, pois existe uma definição única para composição, embora não possa haver apenas uma definição única para uma função de ligação, existe uma por tipo monádico, se bem entendi.
Hibou57
14

Da wikipedia :

Na programação funcional, uma mônada é um tipo de tipo de dado abstrato usado para representar cálculos (em vez de dados no modelo de domínio). Mônadas permitem que o programador encadeie ações para criar um pipeline, no qual cada ação é decorada com regras de processamento adicionais fornecidas pela mônada. Os programas escritos em estilo funcional podem usar mônadas para estruturar procedimentos que incluem operações sequenciadas, 1 [2] ou para definir fluxos de controle arbitrários (como lidar com simultaneidade, continuações ou exceções).

Formalmente, uma mônada é construída definindo duas operações (ligação e retorno) e um construtor de tipo M que deve cumprir várias propriedades para permitir a composição correta das funções monádicas (ou seja, funções que usam valores da mônada como argumentos). A operação de retorno pega um valor de um tipo simples e o coloca em um contêiner monádico do tipo M. A operação de ligação executa o processo inverso, extraindo o valor original do contêiner e passando-o para a próxima função associada no pipeline.

Um programador irá compor funções monádicas para definir um pipeline de processamento de dados. A mônada atua como uma estrutura, pois é um comportamento reutilizável que decide a ordem na qual as funções monádicas específicas no pipeline são chamadas e gerencia todo o trabalho secreto exigido pelo cálculo. [3] Os operadores de ligação e retorno intercalados no pipeline serão executados após cada função monádica retornar o controle e cuidarão dos aspectos específicos tratados pela mônada.

Eu acredito que isso explica muito bem.

the_drow
fonte
12

Vou tentar fazer a definição mais curta que posso gerenciar usando termos de POO:

Uma classe genérica CMonadic<T>é uma mônada se definir pelo menos os seguintes métodos:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}

e se as leis a seguir se aplicarem a todos os tipos T e seus possíveis valores t

identidade esquerda:

CMonadic<T>.create(t).flatMap(f) == f(t)

identidade certa

instance.flatMap(CMonadic<T>.create) == instance

associatividade:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Exemplos :

Uma mônada da lista pode ter:

List<int>.create(1) --> [1]

E o flatMap na lista [1,2,3] poderia funcionar assim:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iteráveis ​​e Observáveis ​​também podem ser monádicos, assim como Promessas e Tarefas.

Comentário :

Mônadas não são tão complicadas. A flatMapfunção é muito parecida com a mais encontrada map. Ele recebe um argumento de função (também conhecido como delegado), que pode chamar (imediatamente ou mais tarde, zero ou mais vezes) com um valor proveniente da classe genérica. Ele espera que a função passada também envolva seu valor de retorno no mesmo tipo de classe genérica. Para ajudar nisso, ele fornece createum construtor que pode criar uma instância dessa classe genérica a partir de um valor. O resultado do retorno do flatMap também é uma classe genérica do mesmo tipo, geralmente compactando os mesmos valores que estavam contidos nos resultados do retorno de uma ou mais aplicações do flatMap nos valores anteriormente contidos. Isso permite que você conecte o flatMap quantas vezes quiser:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

Acontece que esse tipo de classe genérica é útil como modelo básico para um grande número de coisas. Este (juntamente com os jargonismos da teoria das categorias) é a razão pela qual as mônadas parecem tão difíceis de entender ou explicar. Eles são uma coisa muito abstrata e só se tornam obviamente úteis quando são especializados.

Por exemplo, você pode modelar exceções usando contêineres monádicos. Cada contêiner conterá o resultado da operação ou o erro que ocorreu. A próxima função (delegar) na cadeia de retornos de chamada flatMap somente será chamada se a anterior empacotou um valor no contêiner. Caso contrário, se um erro foi compactado, o erro continuará a se propagar pelos contêineres encadeados até que seja encontrado um contêiner que tenha uma função de manipulador de erros anexada por meio de um método chamado .orElse()(esse método seria uma extensão permitida)

Notas : Os idiomas funcionais permitem escrever funções que podem operar em qualquer tipo de classe genérica monádica. Para que isso funcione, seria necessário escrever uma interface genérica para as mônadas. Eu não sei se é possível escrever essa interface em c #, mas tanto quanto eu sei que não é:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad
}
Gorgi Kosev
fonte
7

Se uma mônada tem uma interpretação "natural" no OO depende da mônada. Em uma linguagem como Java, você pode traduzir a mônada talvez para a linguagem de verificação de ponteiros nulos, para que os cálculos que falham (ou seja, produzam Nada em Haskell) emitam ponteiros nulos como resultado. Você pode converter a mônada do estado no idioma gerado, criando uma variável mutável e métodos para alterar seu estado.

Uma mônada é um monóide na categoria de endofunitores.

As informações que a frase reúne são muito profundas. E você trabalha em uma mônada com qualquer linguagem imperativa. Uma mônada é um idioma específico do domínio "sequenciado". Satisfaz certas propriedades interessantes que, juntas, fazem de uma mônada um modelo matemático de "programação imperativa". Haskell facilita a definição de linguagens imperativas pequenas (ou grandes), que podem ser combinadas de várias maneiras.

Como programador de OO, você usa a hierarquia de classes da sua linguagem para organizar os tipos de funções ou procedimentos que podem ser chamados em um contexto, o que você chama de objeto. Uma mônada também é uma abstração dessa idéia, na medida em que mônadas diferentes podem ser combinadas de maneiras arbitrárias, "importando" efetivamente todos os métodos da submonada para o escopo.

Arquitetonicamente, usa-se assinaturas de tipo para expressar explicitamente quais contextos podem ser usados ​​para calcular um valor.

Pode-se usar transformadores de mônada para esse fim, e há uma coleção de alta qualidade de todas as mônadas "padrão":

  • Listas (cálculos não determinísticos, tratando uma lista como um domínio)
  • Talvez (cálculos que podem falhar, mas para os quais os relatórios não são importantes)
  • Erro (cálculos que podem falhar e requerem tratamento de exceção
  • Leitor (cálculos que podem ser representados por composições de funções simples de Haskell)
  • Writer (cálculos com "renderização" / "log" seqüencial (para strings, html etc)
  • Cont (continuações)
  • IO (cálculos que dependem do sistema de computador subjacente)
  • Estado (cálculos cujo contexto contém um valor modificável)

com transformadores de mônada correspondentes e classes de tipo. As classes de tipo permitem uma abordagem complementar para combinar mônadas unificando suas interfaces, para que mônadas concretas possam implementar uma interface padrão para o "tipo" de mônada. Por exemplo, o módulo Control.Monad.State contém uma classe MonadState sm e (State s) é uma instância do formulário

instance MonadState s (State s) where
    put = ...
    get = ...

A longa história é que uma mônada é um functor que anexa "contexto" a um valor, que tem uma maneira de injetar um valor na mônada e que tem uma maneira de avaliar valores em relação ao contexto anexado a ela, pelo menos de maneira restrita.

Assim:

return :: a -> m a

é uma função que injeta um valor do tipo a em uma "ação" de mônada do tipo m a.

(>>=) :: m a -> (a -> m b) -> m b

é uma função que executa uma ação de mônada, avalia seu resultado e aplica uma função ao resultado. O interessante de (>> =) é que o resultado está na mesma mônada. Em outras palavras, em m >> = f, (>> =) retira o resultado de m e o vincula a f, de modo que o resultado esteja na mônada. (Como alternativa, podemos dizer que (>> =) puxa f para m e aplica-o ao resultado.) Como conseqüência, se tivermos f :: a -> mb eg g :: b -> mc, podemos ações de "sequência":

m >>= f >>= g

Ou, usando "notação"

do x <- m
   y <- f x
   g y

O tipo para (>>) pode estar iluminado. Isto é

(>>) :: m a -> m b -> m b

Corresponde ao operador (;) em linguagens procedurais como C. Permite notação como:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

Na lógica matemática e filosófica, temos quadros e modelos, que são "naturalmente" modelados com monadismo. Uma interpretação é uma função que examina o domínio do modelo e calcula o valor de verdade (ou generalizações) de uma proposição (ou fórmula, sob generalizações). Numa lógica modal da necessidade, poderíamos dizer que uma proposição é necessária se for verdadeira em "todo mundo possível" - se for verdadeira em relação a todos os domínios admissíveis. Isso significa que um modelo em uma linguagem para uma proposição pode ser reificado como um modelo cujo domínio consiste na coleção de modelos distintos (um correspondente a cada mundo possível). Toda mônada possui um método chamado "join", que nivela as camadas, o que implica que toda ação de mônada cujo resultado é uma ação de mônada pode ser incorporada na mônada.

join :: m (m a) -> m a

Mais importante, isso significa que a mônada é fechada na operação "empilhamento de camadas". É assim que os transformadores de mônada funcionam: eles combinam mônadas, fornecendo métodos "semelhantes a junções" para tipos como

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

para que possamos transformar uma ação em (MaybeT m) em uma ação em m, efetivamente recolhendo camadas. Nesse caso, runMaybeT :: MaybeT ma -> m (Maybe a) é o nosso método de junção. (MaybeT m) é uma mônada, e MaybeT :: m (Talvez a) -> MaybeT ma é efetivamente um construtor para um novo tipo de ação de mônada em m.

Uma mônada livre para um functor é a mônada gerada pelo empilhamento f, com a implicação de que toda sequência de construtores para f é um elemento da mônada livre (ou, mais exatamente, algo com a mesma forma que a árvore de sequências de construtores para f) Mônadas livres são uma técnica útil para a construção de mônadas flexíveis com uma quantidade mínima de placa de caldeira. Em um programa Haskell, eu poderia usar mônadas gratuitas para definir mônadas simples para "programação de sistema de alto nível" para ajudar a manter a segurança de tipos (estou apenas usando tipos e suas declarações. As implementações são diretas com o uso de combinadores):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

O monadismo é a arquitetura subjacente para o que você pode chamar de padrão "intérprete" ou "comando", abstraído para sua forma mais clara, pois toda computação monádica deve ser "executada", pelo menos trivialmente. (O sistema de tempo de execução executa a mônada de IO para nós e é o ponto de entrada para qualquer programa Haskell. A IO "conduz" o restante dos cálculos, executando as ações de IO em ordem).

O tipo de junção também é onde obtemos a afirmação de que uma mônada é um monóide na categoria de endofunitores. A junção é tipicamente mais importante para fins teóricos, em virtude de seu tipo. Mas entender o tipo significa entender mônadas. Os tipos de união do transformador de junção e do tipo monad são efetivamente composições de endofunitores, no sentido da composição da função. Para colocá-lo em uma pseudo-linguagem semelhante a Haskell,

Foo :: m (ma) <-> (m. M) a

nomenclatura
fonte
3

Uma mônada é uma matriz de funções

(Pst: uma matriz de funções é apenas uma computação).

Na verdade, em vez de uma matriz verdadeira (uma função em uma matriz celular), você tem essas funções encadeadas por outra função >> =. O >> = permite adaptar os resultados da função i para alimentar a função i + 1, realizar cálculos entre eles ou, mesmo, não chamar a função i + 1.

Os tipos usados ​​aqui são "tipos com contexto". Ou seja, um valor com uma "tag". As funções encadeadas devem ter um "valor simples" e retornar um resultado marcado. Um dos deveres de >> = é extrair um valor nu do seu contexto. Há também a função "return", que pega um valor nu e o coloca com uma tag.

Um exemplo com Talvez . Vamos usá-lo para armazenar um número inteiro simples no qual fazer cálculos.

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

Apenas para mostrar que as mônadas são uma matriz de funções com operações auxiliares, considere o equivalente ao exemplo acima, apenas usando uma matriz real de funções

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

E seria usado assim:

print (runMyMonad (Just 160) myArray1)
cibercitizen1
fonte
1
Super arrumado! Assim ligam-se é apenas uma maneira de avaliar um conjunto de funções com o contexto, em sequência, de uma entrada com o contexto :)
Musa Al-Hassy
>>=é um operador
user2418306
1
Eu acho que a analogia da "matriz de funções" não esclarece muito. Se \x -> x >>= k >>= l >>= mé uma matriz de funções, o mesmo ocorre h . g . f, o que não envolve mônadas.
usar o seguinte comando
poderíamos dizer que os functores , monádicos, aplicativos ou simples, são sobre "aplicação embelezada" . 'application' adiciona encadeamento e 'monad' adiciona dependência (isto é, criando a próxima etapa de cálculo, dependendo dos resultados de uma etapa anterior).
Will Ness
3

Em termos OO, uma mônada é um contêiner fluente.

O requisito mínimo é uma definição class <A> Somethingque suporta um construtorSomething(A a) e pelo menos um métodoSomething<B> flatMap(Function<A, Something<B>>)

Indiscutivelmente, também conta se a sua classe monad possui algum método com assinatura Something<B> work() que preservam as regras da classe - o compilador utiliza o flatMap no momento da compilação.

Por que uma mônada é útil? Porque é um contêiner que permite operações com capacidade de cadeia que preservam a semântica. Por exemplo, Optional<?>preserva a semântica da IsPresent para Optional<String>, Optional<Integer>, Optional<MyClass>, etc.

Como um exemplo aproximado,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Note que começamos com uma string e terminamos com um número inteiro. Muito legal.

No OO, pode demorar um pouco para acenar com a mão, mas qualquer método em Algo que retorne outra subclasse de Algo atende ao critério de uma função de contêiner que retorna um contêiner do tipo original.

É assim que você preserva a semântica - ou seja, o significado e as operações do contêiner não mudam, apenas envolvem e aprimoram o objeto dentro do contêiner.

Roubar
fonte
2

Mônadas em uso típico são o equivalente funcional dos mecanismos de manipulação de exceção da programação procedural.

Nas linguagens processuais modernas, você coloca um manipulador de exceção em torno de uma sequência de instruções, qualquer uma das quais pode gerar uma exceção. Se alguma das instruções lançar uma exceção, a execução normal da sequência de instruções será interrompida e transferida para um manipulador de exceções.

As linguagens de programação funcional, no entanto, evitam filosoficamente os recursos de manipulação de exceções devido à natureza "goto" deles. A perspectiva da programação funcional é que as funções não devem ter "efeitos colaterais", como exceções que interrompem o fluxo do programa.

Na realidade, efeitos colaterais não podem ser descartados no mundo real devido principalmente à E / S. Mônadas na programação funcional são usadas para lidar com isso, fazendo um conjunto de chamadas de funções em cadeia (qualquer uma das quais pode produzir um resultado inesperado) e transformando qualquer resultado inesperado em dados encapsulados que ainda podem fluir com segurança pelas chamadas de função restantes.

O fluxo de controle é preservado, mas o evento inesperado é encapsulado e tratado com segurança.

David K. Hess
fonte
2

Uma simples explicação de Mônadas com um estudo de caso da Marvel está aqui .

Mônadas são abstrações usadas para sequenciar funções dependentes que são eficazes. Eficaz aqui significa que eles retornam um tipo no formato F [A], por exemplo, Opção [A] onde Opção é F, chamada construtor de tipo. Vamos ver isso em 2 etapas simples

  1. Abaixo A composição da função é transitiva. Então, passar de A para CI pode compor A => B e B => C.
 A => C   =   A => B  andThen  B => C

insira a descrição da imagem aqui

  1. No entanto, se a função retornar um tipo de efeito como Opção [A], ou seja, A => F [B], a composição não funcionará para ir para B, precisamos de A => B, mas temos A => F [B].
    insira a descrição da imagem aqui

    Precisamos de um operador especial, "bind", que saiba fundir essas funções que retornam F [A].

 A => F[C]   =   A => F[B]  bind  B => F[C]

A função "bind" é definida para o F específico .

Também existe "retorno" , do tipo A => F [A] para qualquer A , definido também para esse F específico . Para ser uma Mônada, F deve ter essas duas funções definidas para ela.

Assim, podemos construir uma função eficaz A => F [B] a partir de qualquer função pura A => B ,

 A => F[B]   =   A => B  andThen  return

mas um determinado F também pode definir suas próprias funções especiais "internas" opacas de tais tipos que um usuário não pode definir a si mesmo (em uma linguagem pura ), como

  • "aleatório" ( intervalo => aleatório [Int] )
  • "print" ( String => IO [()] )
  • "tente ... pegar", etc.
Ira
fonte
2

Estou compartilhando minha compreensão das mônadas, o que pode não ser teoricamente perfeito. Mônadas são sobre propagação de contexto . Mônada é: você define algum contexto para alguns dados (ou tipos de dados) e, em seguida, define como esse contexto será transportado com os dados por todo o pipeline de processamento. E definir propagação de contexto é principalmente definir como mesclar vários contextos (do mesmo tipo). Usar Mônadas também significa garantir que esses contextos não sejam acidentalmente removidos dos dados. Por outro lado, outros dados sem contexto podem ser trazidos para um contexto novo ou existente. Então, esse conceito simples pode ser usado para garantir a correção do tempo de compilação de um programa.

Gulshan
fonte
1

Veja minha resposta para "O que é uma mônada?"

Começa com um exemplo motivador, funciona através do exemplo, deriva um exemplo de mônada e define formalmente "mônada".

Ele não assume conhecimento de programação funcional e usa pseudocódigo com function(argument) := expressionsintaxe com as expressões mais simples possíveis.

Este programa C ++ é uma implementação da mônada de pseudocódigo. (Para referência: Mé o construtor de tipos, feedé a operação "bind" e wrapé a operação "return").

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}
Jordânia
fonte
0

De um ponto de vista prático (resumindo o que foi dito em muitas respostas anteriores e artigos relacionados), parece-me que um dos "propósitos" (ou utilidades) fundamentais da mônada é alavancar as dependências implícitas nas invocações de métodos recursivos aka composição da função (ou seja, quando f1 chama f2 chama f3, f3 precisa ser avaliado antes de f2 antes de f1) para representar a composição sequencial de uma maneira natural, especialmente no contexto de um modelo de avaliação lento (ou seja, composição sequencial como uma sequência simples) , por exemplo, "f3 (); f2 (); f1 ();" em C - o truque é especialmente óbvio se você pensar em um caso em que f3, f2 e f1 realmente não retornam nada [seu encadeamento como f1 (f2 (f3)) é artificial, puramente destinado a criar sequência]).

Isso é especialmente relevante quando os efeitos colaterais estão envolvidos, ou seja, quando algum estado é alterado (se f1, f2, f3 não tivesse efeitos colaterais, não importaria em que ordem eles são avaliados; o que é uma grande propriedade de puro linguagens funcionais, para poder paralelizar esses cálculos, por exemplo). Quanto mais funções puras, melhor.

Penso que, desse ponto de vista restrito, as mônadas poderiam ser vistas como açúcar sintático para idiomas que favorecem a avaliação preguiçosa (que avaliam as coisas somente quando absolutamente necessário, seguindo uma ordem que não depende da apresentação do código) e que não têm outros meios de representar a composição sequencial. O resultado final é que seções do código que são "impuras" (ou seja, que têm efeitos colaterais) podem ser apresentadas naturalmente, de maneira imperativa, mas são claramente separadas das funções puras (sem efeitos colaterais), que podem ser avaliado preguiçosamente.

Este é apenas um aspecto, como advertido aqui .

novis
fonte
0

A explicação mais simples que consigo pensar é que as mônadas são uma maneira de compor funções com resultados embelezados (também conhecida como composição de Kleisli). Uma função "embelished" tem a assinatura a -> (b, smth)onde ae bsão tipos (pense Int, Bool) que podem ser diferentes uns dos outros, mas não necessariamente - e smthé o "contexto" ou o "enfeite".

Este tipo de funções também pode ser escrito a -> m bonde mé equivalente ao "embelezamento" smth. Portanto, são funções que retornam valores no contexto (pense em funções que registram suas ações, onde smthestá a mensagem de log; ou funções que executam input \ output e seus resultados dependem do resultado da ação de E / S).

Uma mônada é uma interface ("typeclass") que faz com que o implementador diga como compor essas funções. O implementador precisa definir uma função de composição (a -> m b) -> (b -> m c) -> (a -> m c)para qualquer tipo mque queira implementar a interface (essa é a composição de Kleisli).

Portanto, se dissermos que temos um tipo de tupla (Int, String)representando resultados de cálculos em Ints que também registram suas ações, (_, String)sendo o "embelezamento" - o log da ação - e duas funções increment :: Int -> (Int, String)e twoTimes :: Int -> (Int, String)queremos obter uma função incrementThenDouble :: Int -> (Int, String)que é a composição das duas funções que também levam em consideração os logs.

No exemplo dado, uma implementação em mônada das duas funções se aplica ao valor inteiro 2 incrementThenDouble 2(que é igual a twoTimes (increment 2)) retornaria (6, " Adding 1. Doubling 3.")para resultados intermediários increment 2iguais (3, " Adding 1.")e twoTimes 3iguais a(6, " Doubling 3.")

A partir dessa função de composição Kleisli, pode-se derivar as funções monádicas usuais.

RedPoppy
fonte