Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AltConfMadrid - Error Management and Data Validation with Bow

AltConfMadrid - Error Management and Data Validation with Bow

Tomás Ruiz-López

June 03, 2019
Tweet

More Decks by Tomás Ruiz-López

Other Decks in Programming

Transcript

  1. Error Management and Data Validation with Bow Tomás Ruiz-López Senior

    Software Engineer at 47degrees @tomasruizlopez @tomasruizlopez | #AltConfMadrid | @bow_swift      
  2. Outline 1. Running example 2. Error modeling 3. Optionals 4.

    Result 5. Fail-fast 6. Error accumulation 7. Applicative 8. Conclusions @tomasruizlopez | #AltConfMadrid | @bow_swift      
  3. Running example Validate user input to create a form. First

    and last name must not be empty. Age must be over 18. Document ID must be 8 digits followed by a letter. Phone number must have 9 digits. Email must contain an @ symbol. @tomasruizlopez | #AltConfMadrid | @bow_swift      
  4. Error modeling enum ValidationError: Error { case emptyFirstName(String) case emptyLastName(String)

    case userTooYoung(Date) case invalidDocumentId(String) case invalidPhoneNumber(String) case invalidEmail(String) } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  5. Success modeling public struct Form { let firstName: String let

    lastName: String let birthday: Date let documentId: String let phoneNumber: String let email: String } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  6. Validation functions func validate(firstName: String) throws String { guard !firstName.trimmingCharacters(in:

    .whitespacesAndNewlines).isEmpty else { throw ValidationError.emptyFirstName(firstName) } return firstName } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  7. Using Optionals Modeling errors as absent values func validate(firstName: String)

    String? { guard !firstName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } return firstName } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  8. Encode information in types Use the compiler in your advantage

    @tomasruizlopez | #AltConfMadrid | @bow_swift      
  9. Using Result Problem: let result = Result(catching: { try validate(firstName:

    "") }) result: Result<String, Error> @tomasruizlopez | #AltConfMadrid | @bow_swift      
  10. Using Result func validate(email: String) Result<String, ValidationError> { if email.contains("@")

    { return .success(email) } else { return .failure(.invalidEmail(email)) } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  11. Using Result func nonEmpty(_ string: String, orElse: (String) ValidationError) Result<String,

    ValidationError> { if !string.trimmingCharacters(in: .whitespacesAndNewlines return .success(string) } else { return .failure(orElse(string)) } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  12. Using Result func validate(firstName: String) Result<String, ValidationError> { return nonEmpty(firstName,

    orElse: ValidationError.emptyFirstName) } func validate(lastName: String) Result<String, ValidationError> { return nonEmpty(lastName, orElse: ValidationError.emptyLastName) } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  13. Using Result func validate(birthday: Date, referenceDate: Date) Result<Date, ValidationError> {

    if Calendar.current.date(byAdding: .year, value: 18, to: birthday)! < referenceDate { return .success(birthday) } else { return .failure(.userTooYoung(birthday)) } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  14. Using Result func matches(_ string: String, regExp: NSRegularExpression, orElse: (String)

    ValidationError) Result<String, ValidationError> { if regExp.firstMatch(in: string, options: [], range: NSRange(location:0, length: string.count) nil { return .success(string) } else { return .failure(orElse(string)) } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  15. Using Result func validate(documentId: String) Result { let documentRegEx =

    try! NSRegularExpression(pattern: "\\d{8}[a zA-Z]{1}") return matches(documentId, regExp: documentRegEx, orElse: ValidationError.invalidDocumentId) } func validate(phoneNumber: String) Result { let phoneRegEx = try! NSRegularExpression(pattern: "\\d{9}") return matches(phoneNumber, regExp: phoneRegEx, orElse: ValidationError.invalidPhoneNumber } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  16. Combining Results let firstNameResult = validate(firstName: "Tomás") let lastNameResult =

    validate(lastName: "Ruiz-López") let birthdayResult = validate(birthday: myBirthday) let documentIdResult = validate(documentId: "00000000A") let phoneResult = validate(phoneNumber: "666111222") let emailResult = validate(email: "[email protected]" let form = ??? @tomasruizlopez | #AltConfMadrid | @bow_swift      
  17. Combining Results let firstNameResult = validate(firstName: "Tomás") switch firstNameResult {

    case let .success(name) case let .failure(error) } firstNameResult.map { name in } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  18. Combining Results switch (firstNameResult, lastNameResult) { case let (.success(firstName), .success(lastName))

    case let (.failure(error), _) case let (_, .failure(error)) } API not available (firstNameResult, lastNameResult).map { firstName, lastName in } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  19. Combining Results We can follow two different combination strategies: 1.

    Fail-fast: return the first error found. 2. Error accumulation: provide a list of all errors found. @tomasruizlopez | #AltConfMadrid | @bow_swift      
  20. Either data type Represents the sum of two types. Either.left

    ~ Result.failure Either.right ~ Result.success @tomasruizlopez | #AltConfMadrid | @bow_swift      
  21. Either data type Result can be easily transformed to Either

    and leverage its API: let firstNameResult: Result<String, VerificationError> = validate(firstName: "Tomás") let firstNameEither: Either<VerificationError, String> = firstNameResult.toEither() let backToResult = firstNameEither.toResult() @tomasruizlopez | #AltConfMadrid | @bow_swift      
  22. Either - Fail fast func failFast(firstName: String, lastName: String, birthday:

    Date, documentId: String, phoneNumber: String, email: String) Either { return Either<ValidationError, Form>.map( validate(firstName: firstName).toEither(), validate(lastName: lastName).toEither(), validate(birthday: birthday, referenceDate: Date()).toEith validate(documentId: documentId).toEither(), validate(phoneNumber: phoneNumber).toEither(), validate(email: email).toEither(), Form.init)^ } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  23. Either - Fail fast If all inputs are correct, Form.init

    is invoked with the corresponding values: failFast(firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970 10), documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]") Right(Form(firstName: "Tomás", lastName: "Ruiz-López", birthday: 1970-01-01 00 00 10 +0000, documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]")) @tomasruizlopez | #AltConfMadrid | @bow_swift      
  24. Either - Fail fast If any input is incorrect, the

    first error found is returned: failFast(firstName: "", lastName: "", birthday: Date(), documentId: "1B", phoneNumber: "AABBCC", email: "myemail") Left(First name is empty: "") @tomasruizlopez | #AltConfMadrid | @bow_swift      
  25. Validated data type A data type to represent valid and

    invalid values. Validated.valid ~ Result.success Validated.invalid ~ Result.failure @tomasruizlopez | #AltConfMadrid | @bow_swift      
  26. Validated data type Result can easily be transformed to Validated

    and leverage its API: let firstNameResult: Result<String, ValidationError> = validate(firstName: "Tomás") let firstNameValidated: Validated<ValidationError, String> = firstNameResult.toValidated() @tomasruizlopez | #AltConfMadrid | @bow_swift      
  27. Validated - Error accumulation It models having a single error:

    Validated<ValidationError, Form> @tomasruizlopez | #AltConfMadrid | @bow_swift      
  28. Validated - Error accumulation We need to accumulate all errors

    in the validation process: Problem: Validated<[ValidationError], Form> let illegal = Validated<[ValidationError], Form>.invalid([]) @tomasruizlopez | #AltConfMadrid | @bow_swift      
  29. Make illegal states impossible to represent Again, use the compiler

    in your advantage @tomasruizlopez | #AltConfMadrid | @bow_swift      
  30. Validated - Error accumulation Accumulates errors and guarantees there is

    at least one: Validated<NonEmptyArray<ValidationError>, Form> Or alternatively: Validated<NEA<ValidationError>, Form> Even more succint, with the ValidatedNEA typealias: ValidatedNEA<ValidationError, Form> @tomasruizlopez | #AltConfMadrid | @bow_swift      
  31. Validated data type Result can easily be transformed to ValidatedNEA

    and leverage its API: let firstNameResult: Result<String, ValidationError> = validate(firstName: "Tomás") let firstNameValidated: ValidatedNEA<ValidationError, String> firstNameResult.toValidatedNEA() @tomasruizlopez | #AltConfMadrid | @bow_swift      
  32. Validated - Error accumulation func errorAccummulation(firstName: String, lastName: String, birthday:

    Date, documentId: String, phoneNumber: String, email: String) ValidatedNEA<Validat return ValidatedNEA<ValidationError, Form>.map( validate(firstName: firstName).toValidatedNEA(), validate(lastName: lastName).toValidatedNEA(), validate(birthday: birthday, referenceDate: Date()).toVali validate(documentId: documentId).toValidatedNEA(), validate(phoneNumber: phoneNumber).toValidatedNEA(), validate(email: email).toValidatedNEA(), Form.init)^ } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  33. Validated - Error accumulation If all inputs are correct, Form.init

    is invoked with the corresponding values: errorAccummulation(firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970 10), documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]") Valid(Form(firstName: "Tomás", lastName: "Ruiz-López", birthday: 1970-01-01 00 00 10 +0000, documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]")) @tomasruizlopez | #AltConfMadrid | @bow_swift      
  34. Validated - Error accumulation If some inputs are invalid, they

    are combined and reported: errorAccummulation(firstName: "", lastName: "", birthday: Date(), documentId: "1B", phoneNumber: "AABBCC", email: "myemail") Invalid(NonEmptyArray([User's email is invalid: "myemail", User's phone number is invalid: "AABBCC", User's document id is invalid: "1B", User is too young: 2019-05-27 13 46 31 +0000, Last name is empty: "", First name is empty: ""])) @tomasruizlopez | #AltConfMadrid | @bow_swift      
  35. Summary Data type Extract values Combine values Combination strategy Result<Success,

    Failure> Pattern matching Either<Failure, Success> fold map Fail-fast Validated<Failure, Success> fold map Error accumulation @tomasruizlopez | #AltConfMadrid | @bow_swift      
  36. Applicative With simulated Higher-Kinded Types: Someday, with native Higher-Kinded Types...

    static func ap<A, B>(_ ff: Kind<F, (A) B>, _ fa: Kind<F, A>) Kind<F, B> static func ap<A, B>(_ ff: F<(A) B>, _ fa: F<A>) F<B> @tomasruizlopez | #AltConfMadrid | @bow_swift      
  37. Applicative Perform independent computations and combine their results. Either<ValidationError, Form>.map(

    fa, fb, fc) { a, b, c in } Validated<ValidationError, Form>.map( fa, fb, fc) { a, b, c in } In general, working with any type F F<Form>.map( fa, fb, fc) { a, b, c in } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  38. Lessons learned Use types to model success and error cases.

    Result is nice, but needs a more powerful API. Bow provides Either (fail-fast) and Validated (error accumulation). Make illegal states impossible to represent. Use abstractions based on type classes. @tomasruizlopez | #AltConfMadrid | @bow_swift      
  39. Thanks! Questions? Bow Tomás Ruiz-López: Bow: | 47degrees: | @tomasruizlopez

    @bow_swift bow-swift.io @47deg 47deg.com @tomasruizlopez | #AltConfMadrid | @bow_swift      
  40. Bonus Track I I don't care about the type that

    I'm using a.k.a. Tagless Final @tomasruizlopez | #AltConfMadrid | @bow_swift      
  41. Tagless Final F ApplicativeError F.pure ~ Result.success F.raiseError ~ Result.failure

    @tomasruizlopez | #AltConfMadrid | @bow_swift      
  42. Tagless Final Instances of ApplicativeError Data Type pure raiseError Either

    right left Validated valid invalid @tomasruizlopez | #AltConfMadrid | @bow_swift      
  43. Tagless Final Creating success and failure class ValidationRules<F: ApplicativeError> where

    F.E NEA<ValidationError> { static func validate(email: String) Kind<F, String> { if email.contains("@") { return .pure(email) } else { return .raiseError(.of(.invalidEmail(email))) } } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  44. Tagless Final Combining independent computations class ValidationRules<F: ApplicativeError> { static

    func makeForm(firstName: String, lastName: String, birthday: Date, documentId: String, phoneNumber: String, email: String) Kind<F, Form> { return F.map(validate(firstName: firstName), validate(lastName: lastName), validate(birthday: birthday, referenceDate: Date()), validate(documentId: documentId), validate(phoneNumber: phoneNumber), validate(email: email), Form.init) } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  45. Tagless final Interpretation to Either ValidationRules<EitherPartial<NEA<ValidationError .makeForm( firstName: "Tomás", lastName:

    "Ruiz-López", birthday: Date(timeIntervalSince1970 10), documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]") @tomasruizlopez | #AltConfMadrid | @bow_swift      
  46. Tagless final Interpretation to Validated ValidationRules<ValidatedPartial<NEA<ValidationError .makeForm( firstName: "Tomás", lastName:

    "Ruiz-López", birthday: Date(timeIntervalSince1970 10), documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]") @tomasruizlopez | #AltConfMadrid | @bow_swift      
  47. Bonus Track II I don't need to use NonEmptyArray thanks

    to Semigroup @tomasruizlopez | #AltConfMadrid | @bow_swift      
  48. Semigroup Combine two elements of the same type. @tomasruizlopez |

    #AltConfMadrid | @bow_swift      
  49. Semigroup Extending the error model to capture multiple errors enum

    ValidationError: Error { case emptyFirstName(String) case emptyLastName(String) case userTooYoung(Date) case invalidDocumentId(String) case invalidPhoneNumber(String) case invalidEmail(String) indirect case multiple(first: ValidationError, rest: [ValidationError]) } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  50. Semigroup Instance of Semigroup for ValidationError extension ValidationError: Semigroup {

    func combine(_ other: ValidationError) ValidationError { switch (self, other) { case let (.multiple(first: selfFirst, rest: selfRest), .multiple(first: otherFirst, rest: otherRest)) return .multiple(first: selfFirst, rest: selfRest + [oth case let (.multiple(first: first, rest: rest), _) return .multiple(first: first, rest: rest + [other]) case let (_, .multiple(first: first, rest: rest)) return .multiple(first: self, rest: [first] + rest) default: return .multiple(first: self, rest: [other]) } } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  51. Semigroup + Tagless Final Validation functions class ValidationRules<F: ApplicativeError> where

    F.E ValidationError { static func validate(email: String) Kind<F, String> { if email.contains("@") { return .pure(email) } else { return .raiseError(.invalidEmail(email)) } } } @tomasruizlopez | #AltConfMadrid | @bow_swift      
  52. Semigroup + Tagless final Interpretation to Either ValidationRules<EitherPartial<ValidationError .makeForm( firstName:

    "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970 10), documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]") @tomasruizlopez | #AltConfMadrid | @bow_swift      
  53. Semigroup + Tagless final Interpretation to Validated ValidationRules<ValidatedPartial<ValidationError .makeForm( firstName:

    "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970 10), documentId: "12345678T", phoneNumber: "666111222", email: "[email protected]") @tomasruizlopez | #AltConfMadrid | @bow_swift      