manejo de errores con typelevel.cats

27
Manejo de errores con typelevel.cats

Upload: leandro-bolivar-alfonzo

Post on 22-Jan-2018

61 views

Category:

Technology


1 download

TRANSCRIPT

Manejo de errores con typelevel.cats

El recorrido

● Error y Excepción.

● El excepcionalismo imperativo.

● Encapsular excepciones en Try.

● Errores como parte del dominio.

● Manejo de errores con Scala Either.

● Acumular errores con Validated.

● Reportar errores y aciertos con Ior.

● Conclusiones.

Error y Excepción

● Error: resultado no deseado de la evaluación de un programa.

● Excepción: condición excepcional del programa en tiempo de

ejecución.

El excepcionalismo imperativo

● Durante la evaluación de un método, si se encuentra un error se arroja

una excepción.

● Este paradigma puede tener su origen en:

○ Lenguajes imperativos sin un sistema de tipos robusto.

○ Considerando lo anterior, carencia de tipos de datos (datatypes) que

permitan manejar los errores.

● Se requiere try/catch para manejar los métodos que puedan arrojar

excepciones.

El dominio

case class Book(isbn: String, title: String, author: String, genre: Genre)

trait Genre extends Product with Serializable

object Genre {

case object Fiction extends Genre

case object ScienceFiction extends Genre

case object HistoricNovel extends Genre

case object InvalidGenre extends Genre

}

val theFountainhead = Book("ISBN: 978-1-4028-9462-6","The Fountainhead","Ayn Rand",Genre.Fiction)

val atlasShrugged = Book("ISBN-13 978-1-4028-9462-6","Atlas Shrugged","Ayn Rand",Genre.Fiction)

val theCountOfMontecristo = Book("ISBN-13 978-1-4028-9462","The Count Of Montecristo","Alexandre

Dumas",Genre.HistoricNovel)

val titlelessBook = Book("ISBN-13 978-1-4028-9462-6","","Unknown",Genre.InvalidGenre)

Un libro con isbn, título, autor y género.

El excepcionalismo imperativo

private def validateGenre(g: Genre): Unit =

if ( g == Genre.InvalidGenre ) throw new InvalidParameter("Book has invalid genre")

private def validateIsbn(isbn: String): Unit = isbn match {

case isbnRegex(all @ _*) => ()

case _ => throw new InvalidParameter("isbn has not a valid format")

}

private def validateTitle(title: String): Unit =

if (title.isEmpty || title == null) throw new InvalidParameter("title must not be empty")

private def validateAuthor(author: String): Unit =

if (author.isEmpty || author == null) throw new InvalidParameter("author must not be empty")

class InvalidParameter(message: String) extends Exception(message)

class EmptyBookList(message: String) extends Exception(message)

El excepcionalismo imperativo

def validateBook(book: Book): Book = {

validateGenre(book.genre)

validateIsbn(book.isbn)

validateTitle(book.title)

validateAuthor(book.author)

book

}

def validateBooks(books: List[Book]): List[Book] =

if (books == Nil) throw new EmptyBookList("Book list was empty")

else {

val booksBuffer = new ListBuffer[Book]

for (book <- books) {

try booksBuffer += validateBook(book) catch { case ex: Exception => println(s"Error $ex") }

}

booksBuffer.toList

}

Encapsulando excepciones con Try

● El tipo Try[A] es utilizado para manejar excepciones en métodos.

● Encapsula el resultado en dos subtipos: Success y Failure.

● Success contiene el resultado del método.

● Failure contiene la excepción arrojada.

● Nos permite usar for comprehension.

● Es fail fast en Failure.

Encapsulando excepciones con Try

private def validateGenre(g: Genre): Try[Genre] = Try {

g match {

case Genre.InvalidGenre => throw new InvalidParameter("Book has invalid genre")

case genre => genre

}

}

private def validateIsbn(isbn: String): Try[String] = Try {

isbn match {

case isbnRegex(all @ _*) => isbn

case _ => throw new InvalidParameter("isbn has not a valid format")

}

}

private def validateTitle(title: String): Try[String] = Try {

if (Option(title).forall(_.isEmpty)) throw new InvalidParameter("title must not be empty") else title

}

private def validateAuthor(author: String): Try[String] = Try {

if (Option(author).forall(_.isEmpty)) throw new InvalidParameter("author must not be empty") else author

}

Encapsulando excepciones con Try

import cats.syntax.semigroup._

//Valida los libros y devuelve una lista no vacía en el caso Success

def validateBook(b: Book): Try[NonEmptyList[Book]] =

for {

i <- validateIsbn(b.isbn)

a <- validateAuthor(b.author)

t <- validateTitle(b.title)

g <- validateGenre(b.genre)

} yield NonEmptyList.of(Book(i, t, a, g))

//Combina los resultados exitosos del Try en una lista no vacía

def validateBooks(bs: List[Book]): Try[NonEmptyList[Book]] = bs match {

case Nil => Failure(new EmptyBookList("Book list was empty"))

case books => books map validateBook reduce (_ |+| _)

}

Encapsulando excepciones con Try

"BookValidation" should {

"Validate a book" in {

val validated = validateBook(theFountainhead)

validated should === (Success(NonEmptyList(theFountainhead, Nil)))

}

"Validate books" in {

val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))

validatedBooks should === (Success(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))

}

"Fail fast error on books" in {

val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead))

inside(validatedBooks) { case Failure(ex) => ex.getMessage shouldBe "title must not be empty" }

}

}

Errores como parte del dominio

● Un error en una función no debe tratarse como algo excepcional.

● Excepciones para interacciones con la capa de infraestructura.

● Excepciones rompen con type safety.

● Los errores también son parte del dominio.

//No es cierto que validateBook siempre devuelve un Book

def validateBook(book: Book): Book = {

validateGenre(book.genre)

validateIsbn(book.isbn)

validateTitle(book.title)

validateAuthor(book.author)

book

}

Errores como parte del dominio

sealed trait Error extends Product with Serializable {

val message: String

}

case class InvalidParameter(message: String) extends Error

case class EmptyBookList(message: String) extends Error

Definimos las excepciones anteriormente usadas como ADTs de Error.

Manejo de errores con Scala Either

● El tipo Either[A, B] representa el valor de dos tipos posibles: Left o Right

● Convencionalmente se usa el Left para representar errores y Right para

representar los aciertos.

● Nos facilita manejar los errores modelados como parte del dominio.

● Se puede usar for comprehension.

● Es fail fast en Left.

Manejo de errores con Scala Either

private def validateGenre(g: Genre): Either[InvalidParameter, Genre] = g match {

case InvalidGenre => Left(InvalidParameter("Book has invalid genre"))

case genre => Right(genre)

}

private def validateIsbn(isbn: String): Either[InvalidParameter, String] = isbn match {

case isbnRegex(all @ _*) => Right(isbn)

case _ => Left(InvalidParameter("isbn has not a valid format"))

}

private def validateTitle(title: String): Either[InvalidParameter, String] =

if (Option(title).forall(_.isEmpty)) Left(InvalidParameter("title must not be empty")) else Right(title)

private def validateAuthor(author: String): Either[InvalidParameter, String] =

if (Option(author).forall(_.isEmpty)) Left(InvalidParameter("author must not be empty")) else Right(author)

Manejo de errores con Scala Either

import cats.syntax.semigroup._

//Valida secuencialmente todos los campos, se detiene en el primer error encontrado.

def validateBook(b: Book): Either[InvalidParameter, NonEmptyList[Book]] =

for {

i <- validateIsbn(b.isbn)

a <- validateAuthor(b.author)

t <- validateTitle(b.title)

g <- validateGenre(b.genre)

} yield NonEmptyList.of(Book(i, t, a, g))

//Por ser fail fast, reporta el primer error encontrado pero acumula todos los aciertos.

def validateBooks(bs: List[Book]): Either[Error, NonEmptyList[Book]] = bs match {

case Nil => Left(EmptyBookList("Book list was empty"))

case books => books map validateBook reduce (_ |+| _)

}

Manejo de errores con Scala Either

"BookValidation" should {

"Validate a book" in {

val validated = validateBook(theFountainhead)

validated should === (Right(NonEmptyList(theFountainhead, Nil)))

}

"Validate books" in {

val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))

validatedBooks should === (Right(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))

}

"Fail fast error on books" in {

val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead))

validatedBooks should === (Left(InvalidParameter("title must not be empty")))

}

}

Manejo de errores con Cats Validated

● El tipo Validated[A, B] representa el valor de dos tipos posibles: Valid e

Invalid.

● Desde el punto de vista semántico, es obvio que Invalid es utilizado para

representar los errores y Valid los aciertos.

● Nos facilita manejar los errores modelados como parte del dominio.

● No se puede utilizar en for comprehension.

● Acumula errores en Invalid ( type ValidatedNel[A, B] =

Validated[NonEmptyList[A], B] ).

Manejo de errores con Cats Validated

private def validateGenre(g: Genre): ValidatedNel[InvalidParameter, Genre] = g match {

case InvalidGenre => InvalidParameter("Book has invalid genre").invalidNel

case genre => genre.validNel

}

private def validateIsbn(isbn: String): ValidatedNel[InvalidParameter, String] = isbn match {

case isbnRegex(all @ _*) => isbn.validNel

case _ => InvalidParameter("isbn has not a valid format").invalidNel

}

private def validateTitle(title: String): ValidatedNel[InvalidParameter, String] =

if (Option(title).exists(_.isEmpty)) InvalidParameter("title must not be empty").invalidNel else title.validNel

private def validateAuthor(author: String): ValidatedNel[InvalidParameter, String] =

if (Option(author).exists(_.isEmpty)) InvalidParameter("author must not be empty").invalidNel

else author.validNel

Manejo de errores con Cats Validated

import cats.syntax.cartesian._

import cats.syntax.semigroup._

//Valida en el producto cartesiano, acumula los errores en cada validación.

def validateBook(b: Book): ValidatedNel[InvalidParameter, NonEmptyList[Book]] = (

validateIsbn(b.isbn) |@|

validateAuthor(b.author) |@|

validateTitle(b.title) |@|

validateGenre(b.genre) ) map {

case (isbn, author, title, genre) =>

NonEmptyList.of(Book(isbn, title, author, genre))

}

//Puede acumular todos los errores o acumular todos los aciertos.

def validateBooks(bs: List[Book]): ValidatedNel[Error, NonEmptyList[Book]] = bs match {

case Nil => EmptyBookList("Book list was empty").invalidNel

case books => books map validateBook reduce (_ |+| _)

}

Manejo de errores con Cats Validated

"BookValidation" should {

"Validate books" in {

val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))

validatedBooks should === (Valid(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))

}

"Accumulate errors on books" in {

val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead))

validatedBooks should === (

Invalid(

NonEmptyList(InvalidParameter("title must not be empty"),

InvalidParameter("Book has invalid genre") :: InvalidParameter("isbn has not a valid format") :: Nil)

)

)

}

}

Manejo de errores con Cats Ior

● El tipo Ior[A, B] representa el valor de tres tipos posibles: Ior.Right,

Ior.Left e Ior.Both.

● La convención con Ior.Left e Ior.Right es igual a la de Either con respecto

a la representación de errores y aciertos.

● Ior.Both permite incluir simultáneamente los errores y aciertos

● Nos facilita manejar los errores modelados como parte del dominio.

● Se puede utilizar en for comprehension.

● Es fail fast en Ior.Left y acumula errores en Ior.Both.

Manejo de errores con Cats Ior

private def validateGenre(g: Genre): IorNel[InvalidParameter, Genre] = g match {

case InvalidGenre => Ior.left(NonEmptyList.of(InvalidParameter("Book has invalid genre")))

case genre => Ior.right(genre)

}

private def validateIsbn(isbn: String): IorNel[InvalidParameter, String] = isbn match {

case isbnRegex(all @ _*) => Ior.right(isbn)

case _ => Ior.left(NonEmptyList.of(InvalidParameter("isbn has not a valid format")))

}

private def validateTitle(title: String): IorNel[InvalidParameter, String] =

if (Option(title).exists(_.isEmpty)) Ior.left(NonEmptyList.of(InvalidParameter("title must not be empty")))

else Ior.right(title)

private def validateAuthor(author: String): IorNel[InvalidParameter, String] =

if (Option(author).exists(_.isEmpty)) Ior.left(NonEmptyList.of(InvalidParameter("author must not be empty")))

else Ior.right(author)

Manejo de errores con Cats Ior

import cats.syntax.semigroup._

//Valida los campos secuencialmente, se detiene en el primer error.

def validateBook(b: Book): IorNel[InvalidParameter, NonEmptyList[Book]] =

for {

i <- validateIsbn(b.isbn)

a <- validateAuthor(b.author)

t <- validateTitle(b.title)

g <- validateGenre(b.genre)

} yield NonEmptyList.of(Book(i, t, a, g))

//A diferencia de Validated, puede acumular errores y aciertos en Ior.Both

def validateBooks(bs: List[Book]): IorNel[Error, NonEmptyList[Book]] = bs match {

case Nil => Ior.left(EmptyBookList("Book list was empty"))

case books => books map validateBook reduce (_ |+| _)

}

Manejo de errores con Cats Ior

"BookValidation" should {

"Validate books" in {

val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))

validatedBooks should === (Ior.Right(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))

}

"Fail fast error on books" in {

val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead, atlasShrugged))

validatedBooks should === (

Ior.Both(

NonEmptyList(InvalidParameter("title must not be empty"),InvalidParameter("isbn has not a valid format") :: Nil),

NonEmptyList(theFountainhead, atlasShrugged :: Nil)

)

)

}

}

Conclusiones

● En una función no se deben arrojar excepciones, se pierde transparencia

referencial.

● Se favorece la modelación de errores en el dominio.

● Try es fail fast, no es expresivo en cuanto a los errores que encapsula y

se puede usar cuando se depende de métodos de librerías java.

● Either es fail fast y puede acumular aciertos.

● Validated acumula errores, no se puede usar en for comprehensions y

puede acumular aciertos.

● Ior es el más flexible: es fail fast en Ior.Left, acumula errores en Ior.Both y

puede acumular aciertos en Ior.Right e Ior.Both.

¿Preguntas?