Monads em Java 

Nov/13
4

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:

  1. Um decorador com m√©todos encade√°veis ( todo o m√©todo retorna uma inst√Ęncia do decorador)
  2. e com um método de fábrica estático que constrói decoradores em torno do tipos a serem decorados. Este método é chamado genericamente de Unit ou Return na literatura de monads
  3. e com um m√©todo de inst√Ęncia que permite receber operadores e transformar o valor dentro do decorador. Este m√©todo √© chamado genericamente de Bind na literatura de monads

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.

3 comentários para “Monads em Java”

  1. 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

  2. […] 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 […]

  3. Muito bom – simples e direto!

Comente

Enquete

Sobre quais assuntos gosta de ler neste blog ?

View Results

Loading ... Loading ...

Artigos