Pensamento Funcional: Manipulação de erros funcional com Either ou Option

Exceções funcionais de tipo seguro

Os desenvolvedores Java™ estão acostumados a manipular erros lançando e capturando exceções que não correspondem ao paradigma funcional. Este artigo do Pensamento funcional investiga formas de indicar erros do Java de maneira funcional, enquanto ainda preserva a segurança do tipo, mostra como concluir exceções verificadas com retornos funcionais e apresenta uma abstração útil, denominada Either.

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.

Photo of Neal FordNeal Ford é arquiteto de software e Meme Wrangler na ThoughtWorks, uma consultoria global de TI. Ele também projeta e desenvolve aplicativos, materiais de instrução, artigos para revistas, treinamentos e apresentações em vídeo/DVD e é autor ou editor de livros que abordam várias tecnologias, incluindo o mais recente The Productive Programmer. Sua especialidade é o projeto e a criação de aplicativos corporativos de grande porte. Também é orador internacionalmente aclamado nas conferências de desenvolvedores ao redor do mundo. Conheça seu website website.



29/Jun/2012

Sobre esta série

Esta série visa reorientar sua perspectiva para uma mentalidade funcional, ajudando a encarar problemas comuns de maneiras novas e encontrar maneiras de melhorar sua codificação diária. Ela explora os conceitos de programação funcional, estruturas que permitem programação funcional dentro da linguagem Java, linguagens de programação funcional executadas em JVM e algumas indicações de tendência futura do design da linguagem. A série é voltada para desenvolvedores que conhecem Java e como suas abstrações funcionam, mas têm pouca ou nenhuma experiência na utilização de uma linguagem funcional.

Quando assuntos profundos são investigados, como programação funcional, ocasionalmente surgem ramificações fascinantes. No último artigo, continuei minha minissérie sobre a reformulação dos padrões tradicionais de design do Gang of Four de maneira funcional. Continuarei esse tópico no próximo artigo com uma discussão sobre a correspondência do padrão estilo Scala, mas primeiro, preciso estabelecer um plano de fundo por meio de um conceito chamado Either. Um dos usos de Eitheré um estilo funcional de manipulação de erros, abordado neste artigo. Depois que você entender a mágica que Either pode fazer com os erros, passarei para correspondência de padrões e árvores no próximo artigo.

Em Java, os erros são tradicionalmente manipulados por exceções e pelo suporte da linguagem para sua criação e propagação. E se a manipulação de exceções estruturada não existisse? Muitas linguagens funcionais não oferecem suporte ao paradigma de exceção, portanto, elas devem encontrar formas alternativas de expressar condições de erro. Neste artigo, mostro mecanismos de manipulação de erros de tipo seguro em Java que efetuam o bypass do mecanismo normal de propagação de exceção (com a ajuda, em alguns exemplos, da estrutura do Functional Java).

Manipulação de erros funcional

Se desejar manipular erros em Java sem o uso de exceções, o obstáculo fundamental é a limitação da linguagem de um único valor de retorno a partir dos métodos. No entanto, os métodos podem, é claro, retornar uma única referência de Object (ou subclasse), que pode manter diversos valores. Portanto, eu poderia ativar diversos valores de retorno utilizando Map. Considere o método divide() , na Listagem 1:

Listagem 1. Usando Map para manipular diversos retornos
public static Map<String, Object> divide(int x, int y) {
    Map<String, Object> result = new HashMap<String, Object>();
    if (y == 0)
        result.put("exception", new Exception("div by zero"));
    else
        result.put("answer", (double) x / y);
    return result;
}

Na Listagem 1, criei um bloco de código Map com String como a chave e Object como valor. No método divide() , coloco tanto exceção , para indicar falha; ou answer para indicar sucesso. Os dois modos são testados na Listagem 2:

Listagem 2. Testando o sucesso e a falha com Maps
@Test
public void maps_success() {
    Map<String, Object> result = RomanNumeralParser.divide(4, 2);
    assertEquals(2.0, (Double) result.get("answer"), 0.1);
}

@Test
public void maps_failure() {
    Map<String, Object> result = RomanNumeralParser.divide(4, 0);
    assertEquals("div by zero", ((Exception) result.get("exception")).getMessage());
}

Na Listagem 2, o teste maps_success verifica se a entrada correta existe no Map retornado. O teste maps_failure verifica o caso de exceção.

Essa abordagem apresenta alguns problemas óbvios. Primeiro, o que quer que termine em Map não é de tipo seguro, o que desativa a capacidade do compilador de capturar determinados erros. As enumerações para as chaves melhorariam um pouco essa situação, mas não muito. Segundo, o invocador do método não sabe se a chamada de método foi bem-sucedida, colocando a responsabilidade de verificar o dicionário por possíveis resultados sobre o responsável pela chamada. Terceiro, nada evita que as chaves tenham valores, o que torna o resultado ambíguo.

Preciso de um mecanismo que permita retornar dois (ou mais) valores de forma de tipo seguro.


A classe Either

A necessidade de retornar dois valores distintos ocorre frequentemente em linguagens funcionais e uma estrutura de dados comum utilizada para modelar esse comportamento é a classe Either . Por meio do uso de genéricos no Java, posso criar uma classe Either , como mostrado na Listagem 3:

Listagem 3. Retornando dois valores (de tipo seguro) por meio da classeEither
public class Either<A,B> {
    private A left = null;
    private B right = null;

    private Either(A a,B b) {
        left = a;
        right = b;
    }

    public static <A,B> Either<A,B> left(A a) {
        return new Either<A,B>(a,null);
    }

    public A left() {
        return left;
    }

    public boolean isLeft() {
        return left != null;
    }

    public boolean isRight() {
        return right != null;
    }

    public B right() {
        return right;
    }

    public static <A,B> Either<A,B> right(B b) {
        return new Either<A,B>(null,b);
    }

   public void fold(F<A> leftOption, F<B> rightOption) {
        if(right == null)
            leftOption.f(left);
        else
            rightOption.f(right);
    }
}

Na Listagem 3, Either foi desenvolvido para manter um valorleft ou right (mas nunca ambos). Essa estrutura de dados é denominada união desconectada. Algumas linguagens baseadas em C contêm o tipo de dados union , que pode manter uma instância de diversos tipos diferentes. Uma união desconectada possui slots para dois tipos, mas mantém uma instância para somente um deles. A classe Either possui um construtor privado , tornando a construção responsabilidade de um dos dois métodos estáticos: left(A a) ou right(B b). Os métodos restantes da classe são auxiliares que recuperam e investigam os membros da classe.

Armado com Either, é possível compor o código que retorna tanto uma exceção quanto um resultado legítimo (mas nunca os dois), enquanto retém a segurança do tipo. A convenção funcional comum é que à esquerda de uma classe Either contém uma exceção (se houver) e à direita contém o resultado.

Analisando numerais romanos

Tenho uma classe denominada RomanNumeral (cuja implementação deixarei à imaginação do leitor) e uma classe denominada RomanNumeralParser , que chama a classe RomanNumeral . O método parseNumber() e testes ilustrativos aparecem na Listagem 4:

Listagem 4. Analisando numerais romanos
public static Either<Exception, Integer> parseNumber(String s) {
    if (! s.matches("[IVXLXCDM]+"))
        return Either.left(new Exception("Invalid Roman numeral"));
    else
        return Either.right(new RomanNumeral(s).toInt());
}

@Test
public void parsing_success() {
    Either<Exception, Integer> result = RomanNumeralParser.parseNumber("XLII");
    assertEquals(Integer.valueOf(42), result.right());
}

@Test
public void parsing_failure() {
    Either<Exception, Integer> result = RomanNumeralParser.parseNumber("FOO");
    assertEquals(INVALID_ROMAN_NUMERAL, result.left().getMessage());
}

Na Listagem 4, o método parseNumber() executa uma validação incrivelmente simples (para o propósito de mostra um erro), colocando a condição de erro à esquerda de Either ou o resultado em sua à direita. Os dois casos são mostrados nos testes de unidade.

Essa é uma grande melhoria em relação a passar o Maps. Retenho a segurança do tipo (observe que posso tornar a exceção tão específica quanto desejado); os erros são óbvios na declaração do método por meio dos genéricos; e os meus resultados voltam com um nível extra de via indireta, desempacotando o resultado (tanto exceção quanto resposta) de . Além disso, o nível extra de via indireta ativa a lentidão.

Análise lenta e Functional Java

A classe Either aparece em muitos algoritmos funcionais e é comum no mundo funcional que a estrutura do Functional Java (consulte Recursos) contenha uma implementação de Either que funcionaria nos exemplos da Listagem 3 e 4 . No entanto, ela foi feita para funcionar com outras construções do Functional Java. Portanto, posso utilizar uma combinação de classe Either e P1 do Functional Java para criar uma avaliação de erro lento . Uma expressão lenta é executada sob demanda (consulte Recursos).

No Functional Java, uma classe P1 é um wrapper simples com relação a um único método denominado _1() que não aceita nenhum parâmetro. (Outras variantes — P2, P3e assim por diante — têm diversos métodos.) Uma P1 é utilizada no Functional Java para passar um bloco de códigos sem executá-lo, possibilitando a execução do código em um contexto de sua escolha.

Em Java, exceções são instanciadas assim que uma exceção é lançada . Posso adiar a criação da exceção retornando um método avaliado de forma lenta. Considere o exemplo e os testes associados na Listagem 5:

Listagem 5. Utilizando o Functional Java para criar um analisador lento
public static P1<Either<Exception, Integer>> parseNumberLazy(final String s) {
    if (! s.matches("[IVXLXCDM]+"))
        return new P1<Either<Exception, Integer>>() {
            public Either<Exception, Integer> _1() {
                return Either.left(new Exception("Invalid Roman numeral"));
            }
        };
    else
        return new P1<Either<Exception, Integer>>() {
            public Either<Exception, Integer> _1() {
                return Either.right(new RomanNumeral(s).toInt());
            }
        };
}

@Test
public void parse_lazy() {
    P1<Either<Exception, Integer>> result = FjRomanNumeralParser.parseNumberLazy("XLII");
    assertEquals((long) 42, (long) result._1().right().value());
}

@Test
public void parse_lazy_exception() {
    P1<Either<Exception, Integer>> result = FjRomanNumeralParser.parseNumberLazy("FOO");
    assertTrue(result._1().isLeft());
    assertEquals(INVALID_ROMAN_NUMERAL, result._1().left().value().getMessage());
}

O código na Listagem 5 é semelhante à Listagem 4, com um wrapper P1 extra. No teste parse_lazy , é preciso desempacotar o resultado por meio da chamada para _1() no resultado, o que retorna à direitade Either, a partir do qual posso recuperar o valor. No teste parse_lazy_exception , é possível verificar a presença de um à esquerda e é possível desempacotar a exceção para distinguir sua mensagem.

A exceção (juntamente com seu rastreio de pilha de geração cara) não é criada até que à esquerda de Either seja desempacotado com a chamada _1() . Portanto, a exceção é lenta, possibilitando o adiamento do construtor da exceção.

Fornecendo padrões

A lentidão não é o único benefício de utilizar Either para a manipulação de erros. Outro benefício é que é possível fornecer valores padrão. Considere o código na Listagem 6:

Listagem 6. Fornecendo valores de retorno padrão razoáveis
public static Either<Exception, Integer> parseNumberDefaults(final String s) {
    if (! s.matches("[IVXLXCDM]+"))
        return Either.left(new Exception("Invalid Roman numeral"));
    else {
        int number = new RomanNumeral(s).toInt();
        return Either.right(new RomanNumeral(number >= MAX ? MAX : number).toInt());
    }
}

@Test
public void parse_defaults_normal() {
    Either<Exception, Integer> result = FjRomanNumeralParser.parseNumberDefaults("XLII");
    assertEquals((long) 42, (long) result.right().value());
}

@Test
public void parse_defaults_triggered() {
    Either<Exception, Integer> result = FjRomanNumeralParser.parseNumberDefaults("MM");
    assertEquals((long) 1000, (long) result.right().value());
}

Na Listagem 6, supomos que nunca permito nenhum numeral romano maior que MAX e qualquer tentativa de configurar um maior será padronizada para MAX. O método parseNumberDefaults() assegura que o padrão seja colocado à direita de Either.

Agrupando exceções

Também é possível utilizar Either para agrupar exceções, convertendo a manipulação de exceções estruturada para funcional, conforme mostrado na Listagem 7:

Listagem 7. Capturando exceções de outras pessoas
public static Either<Exception, Integer> divide(int x, int y) {
    try {
        return Either.right(x / y);
    } catch (Exception e) {
        return Either.left(e);
    }
}

@Test
public void catching_other_people_exceptions() {
    Either<Exception, Integer> result = FjRomanNumeralParser.divide(4, 2);
    assertEquals((long) 2, (long) result.right().value());
    Either<Exception, Integer> failure = FjRomanNumeralParser.divide(4, 0);
    assertEquals("/ by zero", failure.left().value().getMessage());
}

Na Listagem 7, tento a divisão, o que possivelmente causa uma ArithmeticException. Caso ocorra uma exceção, ela é agrupada à esquerdade Either; caso contrário, retorno o resultado à direita. O uso de Either possibilita a conversão de exceções tradicionais (incluindo as verificadas) em um estilo mais funcional.

É claro que também é possível agrupar exceções lançadas de forma lenta a partir de métodos chamados, conforme mostrado na Listagem 8:

Listagem 8. Capturando exceções de forma lenta
public static P1<Either<Exception, Integer>> divideLazily(final int x, final int y) {
    return new P1<Either<Exception, Integer>>() {
        public Either<Exception, Integer> _1() {
            try {
                return Either.right(x / y);
            } catch (Exception e) {
                return Either.left(e);
            }
        }
    };
}

@Test
public void lazily_catching_other_people_exceptions() {
    P1<Either<Exception, Integer>> result = FjRomanNumeralParser.divideLazily(4, 2);
    assertEquals((long) 2, (long) result._1().right().value());
    P1<Either<Exception, Integer>> failure = FjRomanNumeralParser.divideLazily(4, 0);
    assertEquals("/ by zero", failure._1().left().value().getMessage());
}

Aninhando exceções

Um dos recursos legais de exceções Java é a capacidade de declarar diversos tipos de possíveis exceções diferentes como parte de uma assinatura de método. Either também pode fazer isso, embora com uma sintaxe cada vez mais complicada. Por exemplo, e se eu precisasse de um método no RomanNumeralParser que permitisse a divisão de dois numerais romanos, mas fosse preciso retornar duas condições de exceção possíveis — um erro de análise ou um erro de divisão? Utilizando os genéricos Java padrão, é possível aninhar exceções, conforme ilustrado na Listagem 9:

Listagem 9. Exceções aninhadas
public static Either<NumberFormatException, Either<ArithmeticException, Double>> 
        divideRoman(final String x, final String y) {
    Either<Exception, Integer> possibleX = parseNumber(x);
    Either<Exception, Integer> possibleY = parseNumber(y);
    if (possibleX.isLeft() || possibleY.isLeft())
        return Either.left(new NumberFormatException("invalid parameter"));
    int intY = possibleY.right().value().intValue();
    Either<ArithmeticException, Double> errorForY = 
            Either.left(new ArithmeticException("div by 1"));
    if (intY == 1)
        return Either.right((fj.data.Either<ArithmeticException, Double>) errorForY);
    int intX = possibleX.right().value().intValue();
    Either<ArithmeticException, Double> result = 
            Either.right(new Double((double) intX) / intY);
    return Either.right(result);
}

@Test
public void test_divide_romans_success() {
    fj.data.Either<NumberFormatException, Either<ArithmeticException, Double>> result = 
        FjRomanNumeralParser.divideRoman("IV", "II");
    assertEquals(2.0,result.right().value().right().value().doubleValue(), 0.1);
}

@Test

public void test_divide_romans_number_format_error() {
    Either<NumberFormatException, Either<ArithmeticException, Double>> result = 
        FjRomanNumeralParser.divideRoman("IVooo", "II");
    assertEquals("invalid parameter", result.left().value().getMessage());
}

@Test
public void test_divide_romans_arthmetic_exception() {
    Either<NumberFormatException, Either<ArithmeticException, Double>> result = 
        FjRomanNumeralParser.divideRoman("IV", "I");
    assertEquals("div by 1", result.right().value().left().value().getMessage());
}

Na Listagem 9, o método divideRoman() primeiro desempacota Either retornado do método parseNumber() original da Listagem 4. Caso tenha ocorrido uma exceção das duas conversões de números, retorno Either à esquerda com a exceção. Em seguida, é preciso desempacotar os valores de número inteiro reais e executar os critérios da minha outra validação. Os numerais romanos não têm o conceito de zero, portanto, fiz uma regra que desaprova a divisão por 1: Se o denominador for 1, empacoto minha exceção e a coloco à esquerdada direita.

Em outras palavras, tenho três slots delineados por tipos: NumberFormatException, ArithmeticExceptione Double. O primeiro à esquerda de Either, mantém o possível NumberFormatException e seu à direita mantém outro Either. O segundo à esquerda de Either contém um possível ArithmeticException e seu à direita contém a carga útil, o resultado. Portanto, a fim de obter a resposta real, é preciso atravessar result.right().value().right().value().doubleValue()! Obviamente, a praticidade dessa abordagem é rapidamente decomposta, mas ela fornece uma maneira de tipo seguro de aninhar exceções como parte da assinatura de classe.


A classe Option

Either é um conceito útil que utilizarei para desenvolver estruturas de dados em formato de árvore no próximo artigo. Uma classe semelhante no Scala, denominada Option, que foi replicada no Functional Java, fornece um caso de exceção mais simples: tanto nenhum, indicando o valor legítimo; quanto alguns, indicando o retorno bem-sucedido. Option é demonstrada na Listagem 10:

Listagem 10. O uso de Option
public static Option<Double> divide(double x, double y) {
    if (y == 0)
        return Option.none();
    return Option.some(x / y);
}

@Test
public void option_test_success() {
    Option result = FjRomanNumeralParser.divide(4.0, 2);
    assertEquals(2.0, (Double) result.some(), 0.1);
}

@Test
public void option_test_failure() {
    Option result = FjRomanNumeralParser.divide(4.0, 0);
    assertEquals(Option.none(), result);

}

Conforme ilustrado na Listagem 10, uma Option contém none() ou some(), semelhante a left e right , em Either , mas específico ao métodos que podem não ter um valor de retorno legítimo.

Tanto Either quanto Option , no Functional Java, são mónades — estruturas de dados especiais que representam o cálculo e são amplamente utilizadas em linguagens funcionais. No próximo artigo, explorarei os conceitos monádicos relacionados a Either e mostrarei como eles possibilitam a correspondência de padrões estilo Scala em casos isolados.


Conclusão

Quando você aprende um novo paradigma, é preciso reconsiderar todas as maneiras familiares de resolver problemas. A programação funcional utiliza diferentes idiomas para relatar condições de erro, sendo que grande parte pode ser replicada em Java, com uma sintaxe reconhecidamente complicada.

No próximo artigo, aprofundarei nas mónades, discutindo alguns usos para esse conceito fascinante, e mostrarei como o humilde Either pode ser utilizado para desenvolver árvores.

Recursos

Aprender

  • The Productive Programmer (Neal Ford, O'Reilly Media, 2008): o livro de Neal Ford discute as ferramentas e práticas que ajudam a melhorar sua codificação de maneira eficiente.
  • Functional Java: é uma estrutura que acrescenta muitos desenvolvimentos de linguagem funcional a Java.
  • Avaliação lenta: Leia mais sobre esta estratégia para a avaliação de expressões.
  • Mónades: Mónades, um assunto lendariamente difícil em linguagens funcionais, serão abordadas em um artigo futuro desta série.
  • Scala: é uma linguagem moderna e funcional em JVM.
  • Procure na livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
  • O blog e particularmente esta série sobre Throwing Away Throws forneceram inspiração e material base para este artigo.
  • Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=823056
ArticleTitle=Pensamento Funcional: Manipulação de erros funcional com Either ou Option
publish-date=06292012