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.
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 null
assim 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 null
para a coisa toda. Seria tedioso verificar manualmente cada busca
null
e 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
Bind
mais 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 Nullable
em 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 Bind
função para vincular uma variável ao conteúdo do nosso Nullable
valor 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
, g
ou h
retorna null) ou ele vai ser o resultado da soma f
, g
eh
juntos. (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 Bind
certeza 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
, g
e h
para 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 Nullable
estrutura para o lambda.
Aqui está o Bind
operador:
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 a
para
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 Bind
operador tratará toda a lógica de verificação nula para nós. Se, e somente se, o valor que chamamos
Bind
for 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 Bind
e obtemos uma variável vinculada ao valor dentro do valor monádico ( fval
,
gval
e hval
no código de exemplo) e podemos usá-los com segurança no conhecimento que Bind
cuidará 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 Bind
operador 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 Bind
classifica 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 Bind
operador 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 do
notaçã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 do
notação, o que significa que você pode basicamente escrever suas próprias linguagens, definindo apenas duas funções!