Em C #, o que é uma mônada?


189

Atualmente, fala-se muito em mônadas. Eu li alguns artigos / postagens de blog, mas não posso ir longe o suficiente com seus exemplos para entender completamente o conceito. O motivo é que as mônadas são um conceito de linguagem funcional e, portanto, os exemplos estão em idiomas com os quais não trabalhei (já que não usei uma linguagem funcional em profundidade). Não consigo entender a sintaxe profundamente o suficiente para seguir os artigos completamente ... mas posso dizer que há algo que vale a pena entender por lá.

No entanto, eu conheço C # muito bem, incluindo expressões lambda e outros recursos funcionais. Eu sei que o C # tem apenas um subconjunto de recursos funcionais e, portanto, talvez as mônadas não possam ser expressas em C #.

No entanto, certamente é possível transmitir o conceito? Pelo menos eu espero que sim. Talvez você possa apresentar um exemplo de C # como base e depois descrever o que um desenvolvedor de C # gostaria que ele pudesse fazer a partir daí, mas não pode, porque a linguagem carece de recursos de programação funcional. Isso seria fantástico, porque transmitiria a intenção e os benefícios das mônadas. Então, aqui está a minha pergunta: Qual é a melhor explicação que você pode dar sobre mônadas para um desenvolvedor de C # 3?

Obrigado!

(EDIT: By the way, eu sei que já existem pelo menos 3 perguntas "o que é uma mônada" no SO. No entanto, eu enfrento o mesmo problema com elas ... então essa pergunta é necessária imo, por causa do desenvolvedor do C # foco. Obrigado.)


Observe que na verdade é um desenvolvedor de C # 3.0. Não confunda com o .NET 3.5. Fora isso, boa pergunta.
Razzie

4
É de salientar que as expressões de consulta LINQ são um exemplo de comportamento monádico em C # 3.
Erik Forbes

1
Eu ainda acho que é uma pergunta duplicada. Uma das respostas em stackoverflow.com/questions/2366/can-anyone-explain-monads link para channel9vip.orcsweb.com/shows/Going+Deep/… , onde um dos comentários tem um exemplo de C # muito bom. :)
jalf

4
Ainda assim, esse é apenas um link de uma resposta para uma das perguntas da SO. Vejo valor em uma pergunta focada em desenvolvedores de C #. É algo que eu perguntaria a um programador funcional que costumava fazer C # se eu conhecesse um, por isso parece razoável perguntar sobre SO. Mas também respeito o seu direito à sua opinião.
Charlie Flowers

1
Uma resposta não é tudo o que você precisa? ;) O que quero dizer é que uma das outras perguntas (e agora essa também, então sim) tinha uma resposta específica em C # (que parece realmente muito bem escrita, na verdade. Provavelmente a melhor explicação que já vi)
jalf

Respostas:


147

A maior parte do que você faz na programação o dia inteiro está combinando algumas funções para criar funções maiores a partir delas. Normalmente, você tem não apenas funções na sua caixa de ferramentas, mas também outras coisas, como operadores, atribuições de variáveis ​​e similares, mas geralmente o seu programa combina muitas "computações" para computações maiores que serão combinadas ainda mais.

Uma mônada é uma maneira de fazer isso "combinação de cálculos".

Normalmente, o seu "operador" mais básico para combinar dois cálculos é ;:

a; b

Quando você diz isso, você quer dizer "primeiro faça a, depois faça b". O resultado a; bé basicamente novamente uma computação que pode ser combinada com mais coisas. Esta é uma mônada simples, é uma maneira de combinar pequenos cálculos com outros maiores. O ;diz que "fazer a coisa à esquerda, em seguida, fazer a coisa à direita".

Outra coisa que pode ser vista como uma mônada em linguagens orientadas a objetos é o .. Muitas vezes você encontra coisas assim:

a.b().c().d()

O .basicamente significa "avaliar o cálculo à esquerda e, em seguida, chamar o método à direita no resultado disso". É outra maneira de combinar funções / cálculos, um pouco mais complicado que ;. E o conceito de encadear as coisas .é uma mônada, já que é uma maneira de combinar duas computações para uma nova computação.

Outra mônada bastante comum, que não possui sintaxe especial, é esse padrão:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

Um valor de retorno -1 indica falha, mas não há uma maneira real de abstrair essa verificação de erro, mesmo se você tiver muitas chamadas de API que precisam combinar dessa maneira. Isso é basicamente apenas outra mônada que combina as chamadas de função pela regra "se a função à esquerda retornou -1, retornamos -1 nós mesmos, caso contrário, chame a função à direita". Se tivéssemos um operador >>=que fizesse isso, poderíamos simplesmente escrever:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

Isso tornaria as coisas mais legíveis e ajudaria a abstrair nossa maneira especial de combinar funções, para que não precisemos nos repetir repetidamente.

E há muitas outras maneiras de combinar funções / cálculos que são úteis como padrão geral e podem ser abstraídas em uma mônada, permitindo que o usuário da mônada escreva códigos muito mais concisos e claros, já que toda a contabilidade e gerenciamento de as funções usadas são feitas na mônada.

Por exemplo, o acima >>=pode ser estendido para "fazer a verificação de erros e, em seguida, chamar o lado direito no soquete que recebemos como entrada", para que não precisemos especificar explicitamente socketmuitas vezes:

new socket() >>= bind(...) >>= connect(...) >>= send(...);

A definição formal é um pouco mais complicada, pois você precisa se preocupar em obter o resultado de uma função como entrada para a próxima, se essa função precisar dessa entrada e como você deseja garantir que as funções combinadas se encaixem. do jeito que você tenta combiná-los em sua mônada. Mas o conceito básico é que você formaliza maneiras diferentes de combinar funções.


28
Ótima resposta! Vou apresentar uma citação de Oliver Steele, tentando relacionar as Mônadas à sobrecarga do operador à la C ++ ou C #: as mônadas permitem sobrecarregar o ';' operador.
Jörg W Mittag

6
@ JörgWMittag Eu li essa citação antes, mas parecia absurda demais. Agora que compreendo mônadas e leio esta explicação de como ';' é um, eu entendi. Mas acho que é realmente uma afirmação irracional para a maioria dos desenvolvedores imperativos. ';' não é mais visto como um operador do que // é para a maioria.
Jimmy Hoffa

2
Tem certeza de que sabe o que é uma mônada? Mônadas não é uma "função" ou computação, existem regras para mônadas.
27417 Luis

No seu ;exemplo: Quais objetos / tipos de dados são ;mapeados? (Pense em Listmapear Tpara List<T>) Como ;mapear morfismos / funções entre objetos / tipos de dados? O que é pure, join, bindpara ;?
Micha Wiedenmann

44

Faz um ano desde que publiquei esta pergunta. Depois de publicá-lo, mergulhei em Haskell por alguns meses. Gostei imensamente, mas coloquei-o de lado no momento em que estava pronto para me aprofundar nas mônadas. Voltei ao trabalho e foquei nas tecnologias que meu projeto exigia.

E ontem à noite, eu vim e reli essas respostas. Mais importante , reli o exemplo C # específico nos comentários de texto do vídeo de Brian Beckman que alguém menciona acima . Foi tão completamente claro e esclarecedor que decidi publicá-lo diretamente aqui.

Por causa desse comentário, não apenas sinto que entendo exatamente o que as mônadas são… Percebo que na verdade escrevi algumas coisas em c # que são mônadas… ou pelo menos muito próximas, e tentando resolver os mesmos problemas.

Então, aqui está o comentário - tudo isso é uma citação direta do comentário aqui por sylvan :

Isso é bem legal. É um pouco abstrato, no entanto. Eu posso imaginar pessoas que não sabem quais mônadas já ficam confusas devido à falta de exemplos reais.

Então, deixe-me tentar cumprir e, para ficar bem claro, darei um exemplo em C #, mesmo que pareça feio. Vou adicionar o equivalente Haskell no final e mostrar o açúcar sintático legal de Haskell, que é onde, na IMO, as mônadas realmente começam a ser úteis.

Ok, então uma das mônadas mais fáceis é chamada de "Mônada Talvez" em Haskell. Em C #, o tipo Maybe é chamado Nullable<T>. É basicamente uma classe minúscula que apenas encapsula o conceito de um valor que é válido e tem um valor ou é "nulo" e não tem valor.

Uma coisa útil para ficar dentro de uma mônada para combinar valores desse tipo é a noção de falha. Ou seja, queremos poder examinar vários valores anuláveis ​​e retornar nullassim que qualquer um deles for nulo. Isso pode ser útil se, por exemplo, você procurar muitas chaves em um dicionário ou algo assim, e no final quiser processar todos os resultados e combiná-los de alguma forma, mas se alguma delas não estiver no dicionário, você quer voltar nullpara a coisa toda. Seria tedioso verificar manualmente cada busca nulle retornar, para que possamos ocultar essa verificação dentro do operador de ligação (que é o ponto das mônadas, ocultamos a contabilidade no operador de ligação, o que facilita o código use, pois podemos esquecer os detalhes).

Aqui está o programa que motiva a coisa toda (definirei Bindmais tarde, isso é apenas para mostrar por que é legal).

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

Agora, ignore por um momento que já existe suporte para fazer isso Nullableem C # (você pode adicionar ints anuláveis ​​e obter nulo se algum for nulo). Vamos fingir que não existe esse recurso, e é apenas uma classe definida pelo usuário sem mágica especial. O ponto é que podemos usar a Bindfunção para vincular uma variável ao conteúdo do nosso Nullablevalor e depois fingir que não há nada estranho acontecendo, e usá-las como ints normais e apenas adicioná-las. Nós envolvemos o resultado em um nulo no final, e que anulável ou será nulo (se qualquer um dos f, gou hretorna null) ou ele vai ser o resultado da soma f, gehjuntos. (isso é análogo a como podemos vincular uma linha em um banco de dados a uma variável no LINQ e fazer coisas com ela, com a Bindcerteza de que o operador garantirá que a variável receba apenas valores de linha válidos).

Você pode jogar com isso e alterar qualquer uma f, ge hpara retornar nulo e você verá que a coisa toda vai retornar nulo.

Então, claramente, o operador de ligação precisa fazer essa verificação para nós e resgatar o retorno nulo se encontrar um valor nulo e, caso contrário, repassar o valor dentro da Nullableestrutura para o lambda.

Aqui está o Bindoperador:

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

Os tipos aqui são exatamente como no vídeo. É preciso um M a ( Nullable<A>na sintaxe C # para esse caso) e uma função de apara M b( Func<A, Nullable<B>>na sintaxe C #) e retorna um M b ( Nullable<B>).

O código simplesmente verifica se o valor nulo contém um valor e, se o extrai e o passa para a função, ele retorna apenas nulo. Isso significa que o Bindoperador tratará toda a lógica de verificação nula para nós. Se, e somente se, o valor que chamamos Bindfor não nulo, esse valor será "repassado" para a função lambda, caso contrário, resgatamos cedo e toda a expressão é nula. Isso permite que o código que escrevemos usando a mônada esteja totalmente livre desse comportamento de verificação nula, apenas usamos Binde obtemos uma variável vinculada ao valor dentro do valor monádico ( fval, gvale hvalno código de exemplo) e podemos usá-los com segurança no conhecimento que Bindcuidará de verificá-los como nulos antes de transmiti-los.

Existem outros exemplos de coisas que você pode fazer com uma mônada. Por exemplo, você pode fazer o Bindoperador cuidar de um fluxo de caracteres de entrada e usá-lo para escrever combinadores de analisadores. Cada combinador de analisador pode então ficar completamente alheio a coisas como rastreamento de retorno, falhas no analisador etc. e apenas combinar analisadores menores como se as coisas nunca desseem errado, seguros com o conhecimento de que uma implementação inteligente Bindclassifica toda a lógica por trás do pedaços difíceis. Mais tarde, talvez alguém adicione log à mônada, mas o código que a utiliza não muda, porque toda a mágica acontece na definição do Bindoperador e o restante do código permanece inalterado.

Finalmente, aqui está a implementação do mesmo código em Haskell ( -- inicia uma linha de comentário).

-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

Como você pode ver, a boa donotação no final faz com que pareça um código imperativo direto. E, de fato, isso ocorre por design. As mônadas podem ser usadas para encapsular todo o material útil na programação imperativa (estado mutável, E / S etc.) e usadas usando essa sintaxe agradável do tipo imperativa, mas por trás das cortinas, são apenas mônadas e uma implementação inteligente do operador de ligação! O legal é que você pode implementar suas próprias mônadas implementando >>=e return. E se você fizer isso, essas mônadas também poderão usar a donotação, o que significa que você pode basicamente escrever suas próprias linguagens, definindo apenas duas funções!


3
Pessoalmente, prefiro a versão da mônada do F #, mas em ambos os casos eles são incríveis.
ChaosPandion

3
Obrigado por voltar aqui e atualizar sua postagem. São acompanhamentos como esses que ajudam os programadores que estão olhando para uma área específica realmente entenderem como colegas programadores finalmente consideram essa área, em vez de simplesmente ter "como faço x em tecnologia y" para sair. Você é cara!
Kappasims #

Eu segui o mesmo caminho que você basicamente e chegou ao mesmo lugar entendendo mônadas, que disseram que esta é a melhor explicação do comportamento vinculativo de uma mônada que eu já vi para um desenvolvedor imperativo. Embora eu ache que você não esteja tocando muito em tudo sobre mônadas, o que é um pouco mais explicado acima por sth.
Jimmy Hoffa

@ Jimmy Hoffa - sem dúvida, você está certo. Eu acho que para realmente entendê-los mais profundamente, a melhor maneira é começar a usá-los bastante e obter experiência . Ainda não tive essa oportunidade, mas espero que em breve.
Charlie Flowers

Parece que a mônada é apenas um nível mais alto de abstração em termos de programação, ou é apenas uma definição de função contínua e não diferenciável em matemática. De qualquer forma, eles não são novos conceitos, especialmente em matemática.
liang

11

Uma mônada é essencialmente um processamento adiado. Se você está tentando escrever um código que tenha efeitos colaterais (por exemplo, E / S) em um idioma que não os permita, e apenas permita computação pura, um problema é dizer: "Ok, eu sei que você não fará efeitos colaterais. para mim, mas você pode calcular o que aconteceria se o fizesse? "

É meio que trapaça.

Agora, essa explicação o ajudará a entender a intenção geral das mônadas, mas o diabo está nos detalhes. Como exatamente é que você calcular as consequências? Às vezes, não é bonito.

A melhor maneira de fornecer uma visão geral de como alguém está acostumado à programação imperativa é dizer que isso o coloca em uma DSL, na qual operações que se parecem sintaticamente com as que você está acostumado fora da mônada são usadas para criar uma função que faria o que você deseja se puder (por exemplo) gravar em um arquivo de saída. Quase (mas não realmente) como se você estivesse construindo código em uma string para depois ser avaliado.


1
Como no livro I Robot? Onde o cientista pede a um computador que calcule as viagens espaciais e pede que pule certas regras? :) :) :) :)
OscarRyz 23/03/09

3
Hmm, uma Mônada pode ser usada para processamento adiado e para encapsular funções de efeito colateral, na verdade essa foi a primeira aplicação real em Haskell, mas na verdade é um padrão muito mais genérico. Outros usos comuns incluem tratamento de erros e gerenciamento de estado. O açúcar sintático (faça em Haskell, Expressões de computação em F #, sintaxe Linq em C #) é apenas isso e fundamental para as mônadas como tais.
quer

@ MikeHadlow: As instâncias de mônada para tratamento de erros ( Maybee Either e) e gerenciamento de estado ( State s, ST s) me parecem instâncias específicas de "Por favor, calcule o que aconteceria se você tivesse [efeitos colaterais para mim]". Outro exemplo seria o não determinismo ( []).
pyon

isso é exatamente certo; com uma (bem, duas) adições de que é uma E DSL, ou seja, DSL incorporada , pois cada valor "monádico" é um valor válido da sua própria linguagem "pura", representando uma "computação" potencialmente impura. Além disso, existe uma construção monádica de "ligação" em sua linguagem pura, que permite encadear construtores puros de tais valores, onde cada um será chamado com o resultado de sua computação anterior, quando toda a computação combinada for "executada". Isso significa que temos a capacidade de ramificar resultados futuros (ou, em qualquer caso, da linha do tempo separada, "executar").
Will Ness

mas para um programador, significa que podemos programar no EDSL enquanto o misturamos com os cálculos puros de nossa linguagem pura. uma pilha de sanduíches multicamadas é um sanduíche multicamada. é que simples.
Will Ness

4

Tenho certeza de que outros usuários postarão em profundidade, mas achei este vídeo útil até certo ponto, mas direi que ainda não estou no ponto de fluência com o conceito, para que eu pudesse (ou devesse) começar a resolver problemas intuitivamente com Mônadas.


1
O que eu achei ainda mais útil foi o comentário que contém um exemplo de C # abaixo do vídeo.
jalf

Não sei quanto a mais útil, mas certamente pôs as idéias em prática.
TheMissingLINQ

0

Você pode pensar em uma mônada como um C # interfaceque as classes precisam implementar . Essa é uma resposta pragmática que ignora toda a matemática teórica da categoria por que você deseja optar por ter essas declarações em sua interface e ignora todos os motivos pelos quais deseja ter mônadas em um idioma que tenta evitar efeitos colaterais, mas achei um bom começo para alguém que entende interfaces (C #).


Você pode elaborar? O que há em uma interface que a relaciona com mônadas?
Joel Coehoorn

2
Acho que o post do blog gasta vários parágrafos dedicados a essa pergunta.
hao

0

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").

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

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

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

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

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.