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

Error Management and Data Validation with Bow

Error Management and Data Validation with Bow

Nearly every iOS application has to handle errors and validate data of some kind. Swift provides some mechanisms like the recently introduced Result type, which enables a more functional way of dealing with errors. In this talk we will explore other data types that Bow provides to address this task and fail fast or error accumulation strategies.

47 Degrees Academy

February 04, 2022
Tweet

More Decks by 47 Degrees Academy

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      