Uma das coisas que me atraia a experimentar o C# era o conceito de Monad. Eu não entendia muito bem o conceito. O material na internet sobre este assunto é muito vago ou muito virado para scala (que também suporta o conceito) ou para Haskell onde é central ao uso da linguagem. Eu pensava que precisava de suporte do compilador para ter monads. De lá para cá comecei a entender que o conceito monad não é assim tão complexo, não precisa da ajuda do compilador, o e que é muito útil. A linguagem – o compilador- não precisa ter suporte especial ao conceito de monad para poder usá-lo, mas realmente ele só brilha nessas circunstâncias.
O conceito de monad em linguagens orientadas a funções como Haskell é implementado de uma outra forma que em linguagems OO. Em haskell é um Functor, em OO ele tem que ser um objeto. Não interessa realmente abordar aqui como é um monad em haskell (isso você pode encontrar na net a rodo), mas sim, como ele é em OO, e particularmente em Java.
O conceito de monad é muito rico, e ha diversos caminhos para chegar nele, o que se segue é apenas uma forma. Um monad não é um objeto, mas uma família de objetos, uma classificação. Um objeto é um monad se ele cumpre algumas regras simples. Um monad é também sempre um tipo de decorador em OO. Portanto é uma especialização do padrão Decorator.
Imaginemos que temos a necessidade de implementar um objeto Box<T>. Uma das propriedades que seriam interessantes para o objeto box é esta:
Se 3 = 2 + 1 <=> Box<Integer> b = new Box<Integer>(2) + new Box<Integer>(1);
Espera propriedade é interessante porque respeita a relação de adição de números. Mas se em vez de integer tivéssemos strings , mesmo assim existiria uma operação +. Contudo, nem todas as linguagens permitem definir operadores, e mesmo que permitam, seria necessário programar a operação + em Box para cada T possível. Isto não é apenas não-prático, é impossível. Mas antes disso repare que precisamos sempre de encapsular o valor no objeto box. O operador new é interessante, mas não é muito prático. A começar não é polimórfico e não permite definir formas diferentes conforme Ts diferentes. Um método estático de fábrica seria melhor. Vamos então reescrever a mesma expressão em termos de um método estático de fábrica:
Se 3 = 2 + 1 <=> Box<Integer> b = Box.valueOf(2) + Box.valueOf(1);
Uma opção para resolver o problema de definir operações, seria encapsular a operação em um outro objeto de forma que Box pudesse chamar esse outro objeto para operar entre o conteúdo de Box. Suponhamos então que temos uma interface Function<Box<T> , R > que apenas define um método
public interface Function <T> { public Box<T> operate (Box<T> a , T b) ;</p> }
Com isto podemos escrever a soma dentro de uma implementação de Function, e passar ao objeto Box. Definimos nosso objeto de função
Function<Integer > function = new Function <Integer> () { public Box<Integer> operate (Box<Integer> a , T b) { return Box.valueOf(a.getValue()+ b); } }
A nossa expressão ficaria:
Se 3 = 2 + 1 <=> Box<Integer> b = Box.valueOf(2) .bind( 1, function)
Precisamos de duas coisas novas. Primeiro precisamos de um acessor para o valor dentro de box, um simples getValue, serve. Sendo que Box é um decorador, podemos pensar que sempre será possível obter o valor decorado. Por outro lado, precisamos de uma operação nova que chamei de bind que recebe o outro valor e uma função para fazer a conta real. No java 8 poderemos usar lambdas para definia função e será mais simples de escrever. Ficará algo como
Se 3 = 2 + 1 <=> Box<Integer> b = Box.valueOf(2) .bind( 1, (a, b) -> Box.valueOf(a.getValue()+ b))
Chegámos então na construção necessária para que a propriedade seja válida. Um Decorador com dois métodos especiais. Um – que chamamos de valueOf – que pega um qualquer valor T a ser decorado e cria o decorador em torno dele. Um outro método – bind – que pega um outro valor de T e uma operação e retorna um objeto já decorado. Repare que bind retorna um Box<T> que contém a operação bind. Logo podemos chamar quantos binds quisermos em cadeia. Isto é o que define um monad. Um monad é então:
Normalmente o objetivo do decorador é implementar outros métodos que o tipo T não tem e é com esses métodos que faremos coisas úteis. O exemplo de Box não é muito poderoso e parece até meio inútil, mas é a implementação do monad Identity. Um monad mais interessante é o monad Maybe (talvez). Algumas implementações usam o nome Option. O conceito é que o decorador irá encapsular um valor de um tipo qualquer, mas também irá ter a possibilidade de não encapsular nenhum valor. Como o valor real está encapsulado as operações serão feitas pelo monad e não precisaremos nos preocupar se existe ou não um valor. Uma implementação possível de Maybe seria :
public class Maybe<T> { public static <X> Maybe<X> valueOf(X object){ return object == null ? absent() : new Maybe<X>(object); } public static<S extends CharSequence> Maybe<S>valueOf(S sequece){ return object == null || object.length() == 0 ? absent() : new Maybe<S>(sequece); } public static<X>Maybe<X> absent(){ return new Maybe<X>(); } private T value; private boolean hasValue; private Maybe(){ this.hasValue = false; } private Maybe(T value){ this.value = value; this.hasValue = true; } public T getValue (){ if (hasValue){ return value; } throw new ValueIsNotPresentException("Value is not present"); } public boolean isAbsent(){ return !hasValue; } public T or (T defaultValue){ return hasValue ? value : defaultValue; } // equals e hashcode omitidos }
Repare que os métodos de fábrica contém lógica para construir o objeto. Esta é a razão porque não queremos usar o construtor e ter um método especial. A ideia é que se passamos um objeto nulo vamos obter um Maybe vazio (absent) se não, iremos obter um maybe que contém o valor. Repare também que o nosso getValue dá erro se o maybe estiver vazio. Isto é interessante e vai nos ajudar muito, embora não pareça. O monad maybe obriga a trabalhar com valores que existem e esconde os que não existem. Quando você quer obter o valor que existe, isso pode não ser possivel. Nesse caso entra o método Or. Este método retorna o valor se ele existe, e se não existe retorna um valor passado como argumento. Repare ainda que temos dois métodos construtores com lógicas diferentes. Isto nos permite, através de constrangimentos de genéricos escolher lógicas especiais conforme os tipos que encapsulamos. Por exemplo, no caso de um CharSequence, determinamos que se ela não tiver nenhum caracter, ela é vazia (absent). Isto é util ao trabalhar com strings. Onde normalmente uma string vazia é equivalente a uma string com referencia null.
Mas como operar sobre maybe ? Podemos implementar o mesmo método bind como fizemos para box. Contudo, aqui as regras são ligeiramente diferentes. Se o valor é vazio, o resultado de qualquer operação é também vazio. Assim a implementação de bind para maybe seria :
public Maybe<T> bind ( T other, Function<T> f) { Maybe<T> b = Maybe.valueOf(other); return this.hasValue && b.hasValue ? f.operate( this, other); }
Repare que a escolha de receber T em vez de Maybe<T> nos atrapalha um pouco. Deveriamos refactorar o método bind para receber um Maybe<T>. Nesse caso ficaria:
public Maybe<T> bind (Maybe< T> other, Function<T> f) { return this.hasValue && other.hasValue ? f.operate( this, other.getValue()); }
Muito mais simples. Repare que ha um teste se o valor é vazio ou não antes de chamar getValue, logo a exceção nunca será lançada. com este método podemos fazer os seguintes cálculos (assumindo que function é a soma)
Maybe<Integer> a = Maybe.valueOf(2); Maybe<Integer> b = Maybe.valueOf(1); Maybe<Integer> c = Maybe.valueOf(3); Maybe<Integer> nada = Maybe.<Integer>absent(); assertEquals( c, a.bind(b, function)); assertTrue( a.bind(nada, function).isAbsent());
Repare como faz sentido. a + b é c e a + nada é nada. Em java este código é um pouco extenso de escrever. Em java 8 com lambdas pode ficar mais simples de escrever a função que se passa em bind, mas não ha como remover a chamada a bind. Uma operação interessante do maybe que faltou aqui é poder converter o valor dentro do maybe para outro valor. Para isso definimos outra função que recebe T e retorna R onde R pode ser outro tipo diferente de T. O método funciona assim
public <R> Maybe<R> map( Function<T, R> mapper){ return this.hasValue ? Maybe.valueOf(mapper.operate(this.value)) : Maybe.<R>absent(); }
E se usa assim ( com lambdas)
Maybe<String> nome = ... // obtém valor. por exemplo : Maybe.valueOf("Sérgio"); ou Maybe.valueOf(""); Maybe<Integer> tamanho = nome.map( s -> s.length()); if ( !tamanho.isAbsent() ){ System.out.println("Seu nome tem tamanho " + tamanho.getValue()); } else { System.out.println("Seu nome é vazio "); }
Seja como for que o nome é obtido e seja ele vazio ou não, todas as operações funcionam. Se ele é vazio a operação map irá retorna um Maybe<Integer> vazio. Depois determinados se o tamanho é vazio, e se não for apresentamos o tamanho. Caso contrário dizemos que é vazio.
Se lambdas ainda é possivel usar o método map, mas termos que escrever muito mais. teremos que escrever uma instância anônima da interface Function<T,R> e isso não é prático. Em java 8, com lambdas, assim como em outras linguagens que suportam lambdas é muito mais simples e por isso útil.
Você deve ter reparados que lambdas estão ligados ao conceito de monad e embora eles não sejam essenciais ( pois podemos usar objetos normais como sempre fizemos até aqui) não é prático usar monads quando demora mais para escrever a função que usar o método bind. Genérics é outro conceitos importante para monads. Sem ele não ha como controlar o que está dentro do monad , nem construir métodos de fábrica que façam coisas diferentes conforme o tipo a ser encapsulado.
Monads em Java param aqui. Como o compilador não da suporte especial a este conceito, não ha nada mais que podemos fazer. Contudo é importante entender porque bind é importante e como o compilador pode tirar proveito disso. Como vimos todos os métodos de um monad são encadeáveis. O que significa que após chamar bind, podemos chamar de novo, por exemplo :
Maybe<Integer> r = a.bind( b, function).bind(c, function)
Lembrando do principio de por quê construimos este conceito de monad era sobre poder encapsular objetos dentro de outros, mas mesmo assim poder operar com os valores originais. Imagine que o compilador entende o conceito de monad. Então ele sabe que o monad contém um método bind. Se estipulassemos uma assinatura única para o método que o compilador pudesse reconhecer, então ele poderia oferecer uma sintaxe mais simples para invocar o método bind. Para somar dois numeros com maybe, em vez de :
Maybe<Integer> a = Maybe.valueOf(2); Maybe<Integer> b = Maybe.valueOf(1); Maybe<Integer> c = a.bind(b, function));
Poderiamos fazer ( usando um sintaxe hipotética que o compilador entenderia)
Maybe<Integer> a = Maybe.valueOf(2); Maybe<Integer> b = Maybe.valueOf(1); Maybe<Integer> c = for { x <- a, y <- b } return x + y
Esta sintaxe significaria : para o x que está dentro de a, e para o y dentro de b compute x + y e coloque dentro do Maybe. Esta sintaxe é chamada de do-notation ( “notação faça”) e cada uma das linguagem que suportam monads tem a sua forma. Em Scala é :
val c = for { x <- a, y <- b } yield x + y
em C # é :
var c = from x in a from y in b select x + y
Mesmo com a sintaxe especial não ha obrigatoriedade de a usarmos e na realidade o uso da do-notation não é assim tão necessário se você pode simplesmente encadear os métodos. O método bind não precisa se chamar bind, pode ter qualquer nome. Em scala ele se chama flatMap, em C# SelectMany. O ponto é que a assinatura completa do método é conhecida pelo compilador e ele pode escrever expressões complexas sozinho interpretando a do-notation. Contudo, o programador pode usar os métodos explicitamente se quiser. Em java, esta seria a única opção. Se chamarmos o método de apply em vez de bind seria mais fácil de ler pois pareceria que realmente estamos aplicando uma operação ao objeto. Especialmente se estivermos usando lambdas e função for em termos de T e não do monad.
Maybe<Integer> a = Maybe.valueOf(2); Maybe<Integer> b = Maybe.valueOf(1); Maybe<Integer> c = a.apply (b, (x, y) -> x + y));
Espero que tenha entendido que o conceito de monad é bem simples em OO. Nada mais é que uma generalização do padrão Decorator que conta com certas operações especiais sendo a mais especial a que permite operar sobre o conteúdo do decorador. O suporte do compilador não é necessário, embora seja prático em certas circunstâncias. O conceito de monad se utiliza fortemente dos conceitos de generics e é mais simples de usar quando a linguagem dá suporte a lambdas. Com o java 8 trazendo os lambdas o uso de mondas se tornará mais comum.
Existem muitos mais monads que o maybe. Outros são o Writer, o Reader , o Either e o Collection, entre outros. O monad collection não é uma coleção. O monad collection é o que foi implementado no java 8 com o nome de stream e pode ser entendido como um iterador ao qual você pode concatenar outros iteradores e/o outros elementos. O Writer é um monad muito util e se conecta com o conceito de Builder, especialmente, builders de interface fluente. Além deste posso ainda citar o monad State, que faz par com o padrão State. Repare que todos os padrões de projeto que se relacionam a monad podem ser entendidos como uma especialização de Decorador.
Então, se você é um programador java você pode desde já usar o conceito de Monad. Se você quiser o suporte do compilador a do-notation você terá que migrar para outra linguagem, como scala, ou esperar por uma futura versão do java onde isso seja incluído. Este é uma das razões porque alguns já migraram.
Bom dia Sergio Taborda.
Achei legal seu site.Estou elaborando um Termo de Referência para a contratação de um Sistema de Contabilidade Pública e surgiu uma dúvida referente ao preço médio de um Programador por hora de desenvolvimento no caso de customização de algum item específico,Você sabe quanto é esse valor hoje? Ou pode me indicar um site que esclareça isso ?
Desde já agradeço a atenção dispensada.
Ivo
iguimara@rio.rj.gov.br ou
venerotti@uol.com.br
Resposta
[…] conceito de Stream não é novo. Ele advém do conceito de Monad e está presente em linguagens como Lisp e e em linguagens mais modernas como Scala e C# ( o famoso […]
Muito bom – simples e direto!