Monads em C#

Publicado por Sergio Taborda
13/11/2013
Categoria:
Tags: , , ,

As linguagens vão evoluindo conforme os construtos que os seus idealizadores conseguem abstrair. Desde o assembly com suas sequencias e jumps até às linguagens orientadas a objetos, usando estruturas com dados e métodos, passando pelas ideia de rotina, função e método, muitos construtos foram sendo desenvolvidos e a cada passo as linguagens vão ficando mais poderosas. Atualmente a bola da vez é o conceito de Monad (Mônade em português). Linguagens como Scala e C# suportam nativamente este conceito no nível do compilador e isso permite construir código de uma forma muito interessante.

Vamos investigar um pouco o conceito de Monad e como podemos implementar qualquer Monad em C#. Particularmente iremos ver  também como este conceito está relacionado em .NET com o LINQ.

O que é um Monad ? Um Monad em OO nada mais é que um decorador de tipo genérico com operações encadeáveis que possui um contrato para operar sobre o valor decorado.  Estes decoradores oferecem métodos novos que o tipo decorado não tem (como seria de esperar de um decorador), mas também oferecem um contrato que permite operar sobre o valor dentro do decorador.

Para visualizar melhor este conceito pense num objeto decorador que permite encapsular qualquer tipo T:

public class Box<T>
{
     public T Value { get; private set;}
     public Box(T value ){
          this.Value = value;
    }
}

em particular poderia encapsular um inteiro:

var one = new Box(1);
var two = new Box(2);

Para que box seja um monad é necessário que ele permita operar sobre os valores contidos no decorador, então, deveria ser possivel escrever algo como:

var one = new Box(1);
var two= new Box(2);
var three = one + two;

Esta  forma seria interessante mas apresenta problema técnicos. A operação + é especifica do tipo int. Se encapsularmos outro tipo T qualquer, não há garantia que poderiamos usar esta operação. Definir um subtipo de Box para cada tipo de T seria impossivel pois existem, em tese, infinitos tipos. A forma de possibilitar isso é usar um lambda que permite operar sobre os valores de T explicitamente. Para isso acrescentamos um novo método em Box

public Box<R> Select<R> (Box<T> other, Func<T, T , R> f){
    return f(this.value, other.value);
}

e usamos assim:

var three = one.Select( two , (x, y) => x + y)

Porque usamos um lambda podemos sempre operar sobre os valores do tipo decorado. Extrapolando esta regra podemos estabelecer que para que um tipo M<T> seja um Monad é necessário:

  1. Uma forma de decorar o tipo alvo. Isto pode ser feito via construtor, mas métodos de extensão são mais práticos pois permitem polimorfismo e uso de tipos  genéricos.
  2. Uma forma de operar sobre o tipo alvo. Isto pode ser feito de diferentes formas, vimos uma,  veremos depois a forma padrão para o C#

Falamos que para um monad ser útil e interessante ele tem que prover métodos que o tipo decorado não tem. O nosso objeto Box não é muito interessante pois não nos dá nenhum método especial. Contudo ele segue as duas regras acima, e portanto é um Monad. Na realidade é um monad chamado Identity Monad.

Vamos voltar a nossa atenção para outro monad, o Maybe. O Monad Maybe, também chamado de Option, decora um valor de um tipo qualquer T e permite ter objetos que contém valores de T e objetos que não contém valor algum. Este valor especial do monad Maybe se chama Nothing. Você pode pensar nisso como o encapsulamento de null.

Desta vez usaremos métodos de extensão e deixaremos o construtor privado. Até porque, precisamos construir dois tipos diferentes do mesmo monad. Tradicionalmente é usada herança para isso. Existem outras formas de fazer, mas utilizaremos aqui a forma clássica. Então, teremos um tipo Maybe, e duas subclasses: Nothing que encapsula nada; e Just que encapsula um valor de um tipo qualquer T. Utilizaremos para definir o tipo uma interface. Isto é porque utilizar uma interface nos permite definir a covariância de forma que seja verdade que um Maybe<T> é um Maybe<S> se S for um supertipo de T. Infelizmente até à versão corrente do .NET (4.5) só é possível tornar interfaces covariantes e não tipos (ou struts). Contudo, se não precisar de covariância ou não quer se preocupar com isso pode usar uma classe abstrata.

public interface IMaybe<out T>
{
  T Value { get; }
  bool HasValue { get; }
}

public class Just<T> : IMaybe<T>
{
  internal Just(T value)
  {
    this.Value = value;
  }

  public T Value { get; private set; }

  public bool HasValue
  {
     get { return true; }
  }
}

public class Nothing<T> : IMaybe<T>
{
  internal Nothing() { }
  public T Value { get { throw new Exception("No value is present."); } }

  public bool HasValue
  {
    get { return false; }
  }
}

public static class MaybeExtentions
{
  pubic static IMaybe<T> Nothing<T>()
  {
    return new Nothing<T>();
  }

  private static IMaybe<T> Just<T>(T value)
  {
    return new Just<T>(value);
  }

  public static IMaybe<T> ToMaybe<T>(this T obj)
  {
    return obj == null ? Nothing<T>() : Just<T>(obj);
  }

  public static IMaybe<string> ToMaybe<T>(this string obj)
  {
    return string.IsNullOrEmpty(obj) ? Nothing<string>() : Just<string>(obj);
  }

  public static IMaybe<R> Select<R, T>(this IMaybe<T> m, IMaybe<T> other, Func<T, T, R> f)
  {
    return m.HasValue && other.HasValue ? f(m.Value, other.Value).ToMaybe() :  Nothing<R>();
  }
}

Começamos por definir o nosso tipo IMaybe<T>, depois temos duas classes que implementam a interface. Uma que não contém nenhum valor – Nothing – e uma que contém – Just. Para que IMaybe seja um monad precisamos de pelo menos um método que decore qualquer outro tipo. Usamos métodos de extensão para isso. Os métodos ToMaybe transformam qualquer T em um IMaybe<T>. Repare que para string usamos um outro método que testa se a string é vazia. Isto é muito útil na prática e mostra como o uso de métodos estáticos é mais flexível que o uso de construtores. Repare que a propriedade Value de Nothing lança uma exceção. Então é preciso ter cuidado quando se chama Value do Maybe pois pode ser que ele não contenha valor algum. Precisamos agora do método que permite operar sobre o valor contido no IMaybe. O interessante deste monad é que ele nos permite operar mesmo quando não existe valor para ser operado. Ou seja se tentarmos realizar a mesma soma que antes, mas um dos elementos for Nothing o resultado será Nothing seja qual for a operação.
O método Select que recebe dois monads Maybe realiza esta operação. O método só chama a função que vai operar sobre os valores se ambos valores estão presentes. Este método é simples de entender e seria suficiente para podermos chamar a nossa implementação de Monad.

Contudo o C# nos oferece mais ferramentas. O compilador sabe reconhecer um monad e fornece operadores para operar sobre os valores de forma genérica para qualquer monad se definirmos alguns métodos com uma assinatura especifica.  Estes métodos podem fazer parte da class/interface do monad ou estar disponíveis como métodos de extensão. Tanto faz, o compilador irá reconhecê-los. Os métodos são estes:

public static class MaybeExtentions
   {
             ...

        public static IMaybe<V> SelectMany<T, V>(this IMaybe<T> m, Func<T, IMaybe<V>> k)
        {
          return !m.HasValue ? Nothing<V>() : k(m.Value);
        }

        public static IMaybe<V> SelectMany<T, U, V>(this IMaybe<T> m, Func<T, IMaybe<U>> k, Func<T, U, V> s)
        {
          return m.SelectMany(x => k(x).SelectMany(y => s(x, y).ToMaybe()));         }
        }

Os métodos se chamam SelectMany. O primeiro serve para operações de um só monad e ele é bem parecido com nosso método select. O segundo serve para quando temos dois monads. Com estes novos métodos podemos escrever a nossa soma como :

var one = 1.ToMaybe();
var two = 2.ToMaybe();
// usando o primeiro
var three = one.SelectMany( x => (x + two.Value).ToMaybe());
// usando o segundo
var three = one.SelectMany( x => two , (x, y) => x + y);

O uso do primeiro método para operar entre dois Maybe não é muito natural e ali nem verificamos se o segundo maybe é vazio ou não. O segundo método é mais util para operar com dois maybe pois a verificação do valor é feita internamente.

Repare que para o segundo caso, o primeiro lambda serve para colocar o segundo monad dentro do processo e o segundo lambda é que realmente realiza a operação. Os valores de x e y são do tipo decorado (int no caso) o que significa que o monad sabe extrair os valores de dentro de si corretamente. Porque estamos usando o padrão Maybe, este cálculo funciona mesmo quando um dos lados é Nothing

var one = MaybeExtentions.Nothing<int>();
var two = 2.ToMaybe();
var three = one.SelectMany( x => two , (x, y) => x + y);

O resultado será Nothing. Porque o compilador agora entende que o nosso IMaybe é um monad podemos usar uma outra notação geralmente conhecida como  ”do-notation”  (“computation expression” no mundo .NET):

var three =  from x in one
             from y in two
             select x+y;

Esta sintaxe, que  faz parte do LINQ, deixa mais claro que o x e o y representam os valores dentro do decorador e torna muito mais simples escrever a operação de soma. O suporte do compilador é interessante e pode ajudar, mas é sempre possível chamar os métodos e encadeá-los corretamente para obter o resultado desejado. Esta é a forma preferencial quando usamos os métodos utilitários do monad, então o suporte do compilador acaba não sendo tão importante. O que importa saber é que um Monad é um tipo especial de Decorador que têm um (ou mais) métodos para prover a decoração de tipos (que pode ser polimórfica) e um (ou mais) métodos que permitem operar sobre o valor decorado sem ter que saber qual é. O resto é decorrência dessas duas coisas.

Embora o compilador nos simplifique a vida não é sempre que queremos usar monads para coisas simples como somar. Aliás isso é raro. Estamos mais interessados em usar o monad para funcionalidades mais avançadas que não dependem dos valores. É aqui que entram os métodos especiais do decorador. Vamos definir dois métodos auxiliares do IMaybe ( você pode definir muitos mais, aliás depois que começar a definir métodos auxiliares de qualquer Monad é difícil parar):


      public static T Or<T>(this  IMaybe<T> m, T defaultValue)
      {
        return m.HasValue ? m.Value : defaultValue;
      }

      public static IMaybe<R> Select<R,T> (this  IMaybe<T> m, Func<T, R> f)
      {
        return m.HasValue ? f(m.Value).ToMaybe() : Nothing<R>();
      }

O primeiro método – Or – devolve o valor dentro do maybe, se ele existir, e se não, retorna o valor padrão que foi passado como argumento. O método Select recebe um lambda e permite transformar um IMaybe de um tipo T em um IMaybe de um tipo R qualquer. Eis um exemplo de como usuariamos isto:

string name = ...
var stringLength = name.ToMaybe().Select(s => s.Length() ).Or(0);

Suponha que name é uma variável que contém uma string. Você não tem a certeza se essa variável é null ou não, então você se utiliza do monad maybe. Primeiro decora o valor com ToMaybe(). Depois obtém o tamanho da string usando select e no fim transforma para o valor do tamanho para zero se ele não existir. Sem o maybe este código seria bem maior e com ifs.

string name = ...
var stringLength = 0;
if (name != null)
{
    stringLength = name.Length();
}

Este tipo de if não é uma decisão de negócio, é uma decisão meramente de codificação porque não queremos que dê problema quando o string é null. O Monad Maybe ajuda a remover estas “decisões artificiais” do seu código. Isto ajuda na leitura do seu código e se você gosta de testes unitários e  se preocupa com a cobertura de testes, o uso de maybe ajuda a elevar a cobertura pois existem menos ramos de execução possíveis para serem testados.

O C# contém um tipo chamado Nullable<S> onde S é um struct. Isto permite tratar struts como tendo o valor null, embora não seja possível que struts seja nulos. Nullable é uma implementação do Monad Maybe (mas apenas para structs). O compilador até entende extensões especiais da linguagem para trabalhar com este tipo, contudo por que apenas funciona com struts ele é limitado. A nossa implementação de Maybe funciona para qualquer T, struct ou não.

Maybe é apenas um exemplo de Monad. Existem muitos mais. Um outro monad famoso é Collection. Este monad não representa realmente uma coleção mas sim uma cadeia de valores. Em C# ele é representado pela interface IEnumerable<T>. Porque o IEnumerable<T> é um Monad então as mesmas funcionalidades da linguagem estão disponíveis para ele e por isso o LINQ funciona “out-of-the-box” com IEnumerable<T> ou outro monad como IQueryable<T>. Além do Identity, Maybe e Collection existem ainda outros monads como State, Either, Reader, Writer e Continuation, entre outros.

Monads é um assunto interessante mas é difícil encontrar material inteligível na internet sobre o assunto. A maior parte do material é para Haskell. Se garimpar muito vai achar algo sobre Monads em C#. Quem aborda o assunto de um ponto de vista mais formal acaba usando uma notação pouco intuitiva, mas sempre ha alguém que tenta deixas as coisas um pouco mais simples . Monads em C# são o coração das funcionalidades do LINQ  especialmente do LINQ for Relational Data que junto com o conceito de Expressions Trees que aliado ao Writer Monad, que referi anteriormente, e ao padrão Interpreter permitem transformar lambdas em instruções SQL. O interessante disso é que você pode usar Expression Trees junto com o Writer Monad para criar outro tipo de instruções quaisquer. O Writer Monad é muito interessante e com ele é possível criar builders e parsers em C# que são o primeiro passo para pequenas Domain Specific Languages.

Adicione o conceito no seu código, especialmente o uso de Maybe e verá como seu código fica muito mais curto, simples e poderoso.





Desenvolvido por hacklab/ com WordPress