Back-End

11 mai, 2016

Failjure: tratamento de erro exception-free para Clojure

Publicidade

Eu já escrevi sobre a manipulação de erros em Clojure sem usar exceções, fazendo uso de monads ad-hoc. Naquele artigo, eu também citei a implementação de erro monad do Andrew Brehaut.

Desde então, eu escrevi uma série de projetos que usam manipulação semelhante, e acho que chegou a hora de empacotar tudo em uma biblioteca, que eu estou chamando Failjure.

Como funciona?

Failjure é uma biblioteca de utilitários para trabalhar com falhas computacionais em Clojure. Em vez de lançar exceções, suas funções podem retornar falhas, que podem ser manipuladas manualmente ou com alguns helpers embutidos.

Aqui está um exemplo de código que usa failjure:

;; Escrever funções que retornam falhas

(defn validate-email [email]
    (if (re-matches #".+@.+\..+" email)
      email
      (r/fail "Please enter a valid email address (got %s)" email)))

(defn validate-not-empty [s]
  (if (empty? s)
    (r/fail "Please enter a value")
    s))


;; Usar attempt-all para lidar com falhas

(defn validate-data [data]
  (f/attempt-all [email (validate-email (:email data))
                  username (validate-not-empty (:username data))
                  id (f/try* (Integer/parseInt (:id data)))]
    {:email email
     :username username}
    (f/if-failed [e]
      (log-error (f/message e))
      (handle-error e))))

Aqui, attempt-all é um invólucro em torno da macro domonad do clojure.algo.monad, chamado com uma implementação de erro monad. Ele funciona muito parecido com let, exceto que se em qualquer etapa retornar uma falha, ele tem um curto-circuito, seja retornando o objeto da falha ou se if-failed for fornecido, chamando o corpo de if-failed com a falha como seu argumento.

Objetos Java Exception também são considerados falhas. No exemplo acima, uma chamada que poderia lançar uma exceção é envolta em try*, que pega e retorna a exceção como se if-fail tivesse sido chamado.

Existem também as macros attempt-> e attempt->>, você pode ler mais a respeito no README.

Por que não usar exceções simplesmente?

Essa é uma excelente pergunta. O exemplo acima poderia ser facilmente implementado com exceções, substituindo fail por #(throw (Exception. %)) e attempt-all/if-failed por try/catch. Eu acho que existem duas diferenças importantes a considerar.

A primeira são bons hábitos. Clojure não tem uma instrução return ou equivalente de propósito. É considerado uma má forma de sair de uma função em qualquer lugar exceto no final dela – remover a capacidade de throw exceptions incentiva esse tipo de design de software.

Em segundo lugar está a ergonomia de definir tipos de exceção em Clojure. É uma dor ter que usar besteiras como gen-class para criar novos tipos de Exception. Por outro lado, a criação de uma falha típica é fácil, se você realmente quiser.

(defrecord Disaster [message code])

(extend-protocol f/HasFailed
  Disaster
  (message [self] (:message self))
  (failed? [self] true))

(->Disaster "Algo terrível aconteceu!" :red)

Finalmente, é a diferenciação entre falhas esperadas e as inesperadas. Em Java, exceções podem representar ambas (embora RuntimeExceptions recebam tratamento especial das IDEs). Em um programa usando Failjure, as exceções são somente erros inesperados, pelo menos aqueles que não estão envoltos em try*, e devem ser tratados como tal.

Então vá em frente, dê uma chance! Eu acho que você vai achar de grande ajuda escrever código mais seguro, sem comprometer a pureza ou solidez geral.

***

Adam Bard faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://adambard.com/blog/introducing-failjure/