O Result, uma das propostas mais esperadas para o Swift 5, chegou à linguagem. O Result type
força o programador a tratar explicitamente os casos de falha e sucesso antes que eles possam obter acesso ao valor real. Vamos ver como é implementado-ló, usá-lo e por que foi necessário.
O Result type
destina-se a impor uma maneira segura de manipular erros retornados por uma chamada de função sem recorrer a exceções. Enquanto as exceções fornecem um mecanismo automático para propagação e manipulação de erros, o Result
fornece um mecanismo manual com garantias de tipos mais fortes e mais adequado para o tratamento de erros assíncronos. Similarmente ao tipo Option
, o Result type
é uma monad.
O Result type no Swift 5 é implementado como um enum com dois casos: .success
e .failure
.
public enum Result<Success, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
...
}
É possível definir uma função simples retornando um Result
e manipular seu resultado da seguinte forma:
enum AnErrorType: Error {
case failureReason1
case failureReason2
}
func failableFunction() -> Result<Int, AnErrorType> {
...
if (errorCondition1) {
return .failure(.failureReason1)
}
...
return .success(1)
}
func callFailableFunction() {
...
let result = failableFunction()
switch result {
case .success(let integerResult):
...
case .failure(let error):
switch error {
case .failureReason1:
...
case .failureReason2:
...
}
}
...
}
Além disso, para facilitar a integração com funções existentes, o tipo Result
suporta um inicializador específico que aceita uma closure que pode lançar:
let result = Result { try String(contentsOfFile: configuration) }
map ()
(mapError ()
) permite a transformação automática de um valor (erro) através de uma closure, mas apenas em caso de sucesso (falha), caso contrário o <pre > Result é deixado sem modificações.flatMap ()
(flatMapError ()
) é útil quando você quer transformar seu valor (erro) usando uma closure que retorna ele mesmo umResult
para manipular caso quando a transformação falhe. Em tais casos,map ()
(mapError ()
) retornaria um<Result <Result <... >>
. Com oflatMap ()
(flatMapError ()
), se obtém umResult
simples e reduzido.
Os tipos Result
representam uma melhoria em relação à sintaxe do try catch throw
, que é o mecanismo básico para o tratamento de erros no Swift desde o Swift 2, por diversas razões.
Em primeiro lugar, usando os tipos Result
, torna-se muito mais natural lidar com falhas assíncronas. Um padrão típico para manipular chamadas de função assíncronas no Swift faz uso de retornos de chamada, como no exemplo a seguir:
asyncOperationCall() { (value, error) in
guard error != nil else { self.handleError(error!) }
self.handleValue(value)
}
Como é possível reconhecer facilmente, o uso de retornos de chamada acaba com a finalidade das exceções no Swift, que é a propagação automática e o tratamento de erros. Em vez disso, com retornos de chamada assíncronos, o erro deve ser tratado no lugar, porque quando uma exceção pode ser lançada de um retorno de chamada já é tarde demais para o erro ser propagado automaticamente até a pilha de chamadas. Pelo contrário, um Result
pode ser armazenado e processado em um momento posterior quando alguém tenta usar o valor retornado. Essa propriedade, chamada de tratamento de erro atrasado, não é específica do código assíncrono e pode simplificar muito a sintaxe do Swift, conforme mostrado no exemplo a seguir:
do {
handleOne(try String(contentsOfFile: oneFile))
} catch {
handleOneError(error)
}
do {
handleTwo(try String(contentsOfFile: twoFile))
} catch {
handleTwoError(error)
}
do {
handleThree(try String(contentsOfFile: threeFile))
} catch {
handleThreeError(error)
}
O código acima se traduz no seguinte trecho muito mais legível no Swift 5:
let one = Result { try String(contentsOfFile: oneFile) }
let two = Result { try String(contentsOfFile: twoFile) }
let three = Result { try String(contentsOfFile: threeFile) }
handleOne(one)
handleTwo(two)
handleThree(three)
Outra vantagem do Result
é que sabemos com certeza que recebemos um erro ou um valor. Ao usar um retorno de chamada, temos quatro combinações diferentes nas quais devemos pensar: um valor sem erro; nenhum valor com um erro; um valor com um erro; sem valor e sem erro.
Finalmente, usar um tipo Result
permite restringir o tipo de erro exato que pode ser retornado. No primeiro exemplo que fornecemos acima, essa propriedade nos permitiu enumerar completamente as possíveis causas de erro, ou seja, .failureReason1
e .failureReason2
, ao longo do caminho de tratamento de erros. Pelo contrário, uma função Swift que pode lançar não especifica o tipo que pode ser lançado, então ao manipular a condição de erro em um bloco catch
, você só sabe que seu erro está de acordo com o Error
protocolo.
O Swift 5 está previsto para ser lançado no início de 2019 e se concentra em trazer a estabilidade do ABI para a linguagem.