Error Management and Data Validation with

Bow

Tomás Ruiz-López

Senior Software Engineer at 47degrees

@tomasruizlopez

@47deg | 47deg.com

## Outline 1. Running example 2. Error modeling 3. Optionals 4. Result 5. Fail-fast 6. Error accumulation 7. Applicative 8. Conclusions
## 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.
## Error handling ![Swift book cover](img/swift_programming_language.jpg)
### Error modeling ```swift enum ValidationError: Error { case emptyFirstName(String) case emptyLastName(String) case userTooYoung(Date) case invalidDocumentId(String) case invalidPhoneNumber(String) case invalidEmail(String) } ```
### Success modeling ```swift public struct Form { let firstName: String let lastName: String let birthday: Date let documentId: String let phoneNumber: String let email: String } ```
### Validation functions ```swift func validate(firstName: String) throws -> String { guard !firstName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw ValidationError.emptyFirstName(firstName) } return firstName } ```
### Validation functions ```swift func validate(firstName: String) throws -> String ```
## Using Optionals
### Using Optionals Modeling errors as absent values ```swift func validate(firstName: String) -> String? { guard !firstName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } return firstName } ```
## Encode information in types Use the compiler in your advantage
## Using Result
### Using Result ```swift let result = Result(catching: { try validate(firstName: "") }) ``` Problem: ```swift result: Result<String, Error> ```
### Using Result ```swift func validate(email: String) -> Result<String, ValidationError> { if email.contains("@") { return .success(email) } else { return .failure(.invalidEmail(email)) } } ```
### Using Result ```swift func nonEmpty(_ string: String, orElse: (String) -> ValidationError) -> Result<String, ValidationError> { if !string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return .success(string) } else { return .failure(orElse(string)) } } ```
### Using Result ```swift 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) } ```
### Using Result ```swift 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)) } } ```
### Using Result ```swift 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)) } } ```
### Using Result ```swift 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) } ```
### Combining Results ```swift 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: "tomas.ruiz@47deg.com") let form = ??? ```
### Combining Results ```swift let firstNameResult = validate(firstName: "Tomás") switch firstNameResult { case let .success(name): ... case let .failure(error): ... } firstNameResult.map { name in ... } ```
### Combining Results ```swift 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 ... } ```
### 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.
## Either
### Either data type Represents the sum of two types. - `Either.left` ~ `Result.failure` - `Either.right` ~ `Result.success`
### Either data type Result can be easily transformed to Either and leverage its API: ```swift let firstNameResult: Result<String, VerificationError> = validate(firstName: "Tomás") let firstNameEither: Either<VerificationError, String> = firstNameResult.toEither() let backToResult = firstNameEither.toResult() ```
### Either - Fail fast ```swift 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()).toEither(), validate(documentId: documentId).toEither(), validate(phoneNumber: phoneNumber).toEither(), validate(email: email).toEither(), Form.init)^ } ```
### Either - Fail fast If all inputs are correct, `Form.init` is invoked with the corresponding values: ```swift failFast(firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970: 10), documentId: "12345678T", phoneNumber: "666111222", email: "tomas.ruiz@47deg.com") // Right(Form(firstName: "Tomás", lastName: "Ruiz-López", // birthday: 1970-01-01 00:00:10 +0000, // documentId: "12345678T", // phoneNumber: "666111222", // email: "tomas.ruiz@47deg.com")) ```
### Either - Fail fast If any input is incorrect, the first error found is returned: ```swift failFast(firstName: "", lastName: "", birthday: Date(), documentId: "1B", phoneNumber: "AABBCC", email: "myemail") // Left(First name is empty: "") ```
## Validated
### Validated data type A data type to represent valid and invalid values. - `Validated.valid` ~ `Result.success` - `Validated.invalid` ~ `Result.failure`
### Validated data type Result can easily be transformed to Validated and leverage its API: ```swift let firstNameResult: Result<String, ValidationError> = validate(firstName: "Tomás") let firstNameValidated: Validated<ValidationError, String> = firstNameResult.toValidated() ```
### Validated - Error accumulation It models having a single error: ```swift Validated<ValidationError, Form> ```
### Validated - Error accumulation We need to accumulate all errors in the validation process: ```swift Validated<[ValidationError], Form> ``` Problem: ```swift let illegal = Validated<[ValidationError], Form>.invalid([]) ```
# Make illegal states impossible to represent Again, use the compiler in your advantage
### Validated - Error accumulation Accumulates errors and guarantees there is at least one: ```swift Validated<NonEmptyArray<ValidationError>, Form> // Or alternatively: Validated<NEA<ValidationError>, Form> // Even more succint, with the ValidatedNEA typealias: ValidatedNEA<ValidationError, Form> ```
### Validated data type Result can easily be transformed to ValidatedNEA and leverage its API: ```swift let firstNameResult: Result<String, ValidationError> = validate(firstName: "Tomás") let firstNameValidated: ValidatedNEA<ValidationError, String> = firstNameResult.toValidatedNEA() ```
### Validated - Error accumulation ```swift func errorAccummulation(firstName: String, lastName: String, birthday: Date, documentId: String, phoneNumber: String, email: String) -> ValidatedNEA<ValidationError, Form> { return ValidatedNEA<ValidationError, Form>.map( validate(firstName: firstName).toValidatedNEA(), validate(lastName: lastName).toValidatedNEA(), validate(birthday: birthday, referenceDate: Date()).toValidatedNEA(), validate(documentId: documentId).toValidatedNEA(), validate(phoneNumber: phoneNumber).toValidatedNEA(), validate(email: email).toValidatedNEA(), Form.init)^ } ```
### Validated - Error accumulation If all inputs are correct, `Form.init` is invoked with the corresponding values: ```swift errorAccummulation(firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970: 10), documentId: "12345678T", phoneNumber: "666111222", email: "tomas.ruiz@47deg.com") // Valid(Form(firstName: "Tomás", lastName: "Ruiz-López", // birthday: 1970-01-01 00:00:10 +0000, // documentId: "12345678T", // phoneNumber: "666111222", // email: "tomas.ruiz@47deg.com")) ```
### Validated - Error accumulation If some inputs are invalid, they are combined and reported: ```swift 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: ""])) ```
### 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 |
### Applicative `<*>`
## Applicative With simulated Higher-Kinded Types: ```swift static func ap<A, B>(_ ff: Kind<F, (A) -> B>, _ fa: Kind<F, A>) -> Kind<F, B> ``` Someday, with native Higher-Kinded Types... ```swift static func ap<A, B>(_ ff: F<(A) -> B>, _ fa: F<A>) -> F<B> ```
### Applicative Perform independent computations and combine their results. ```swift 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 ... } ```
# Conclusions
## 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.

Read more

Either API Reference

Read more

Validated API Reference

Read more

Bow Documentation

Thanks!

Questions?

Bow

Bonus Track I

I don't care about the type that I'm using

a.k.a.

Tagless Final

### Tagless Final `F: ApplicativeError` - `F.pure ~ Result.success` - `F.raiseError ~ Result.failure`
### Tagless Final Instances of `ApplicativeError` | Data Type | `pure` | `raiseError` | | --------- |:------:|:------------:| | `Either` | `right` | `left` | | `Validated` | `valid` | `invalid` |
### Tagless Final Creating success and failure ```swift 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))) } } } ```
### Tagless Final Combining independent computations ```swift 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) } } ```
### Tagless final Interpretation to `Either` ```swift ValidationRules<EitherPartial<NEA<ValidationError>>> .makeForm( firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970: 10), documentId: "12345678T", phoneNumber: "666111222", email: "tomas.ruiz@47deg.com") ```
### Tagless final Interpretation to `Validated` ```swift ValidationRules<ValidatedPartial<NEA<ValidationError>>> .makeForm( firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970: 10), documentId: "12345678T", phoneNumber: "666111222", email: "tomas.ruiz@47deg.com") ```

Bonus Track II

I don't need to use NonEmptyArray

thanks to

Semigroup

### Semigroup Combine two elements of the same type.
### Semigroup Extending the error model to capture multiple errors ```swift 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]) } ```
### Semigroup Instance of `Semigroup` for `ValidationError` ```swift 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 + [otherFirst] + otherRest) 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]) } } } ```
### Semigroup + Tagless Final Validation functions ```swift 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)) } } } ```
### Semigroup + Tagless final Interpretation to `Either` ```swift ValidationRules<EitherPartial<ValidationError>> .makeForm( firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970: 10), documentId: "12345678T", phoneNumber: "666111222", email: "tomas.ruiz@47deg.com") ```
### Semigroup + Tagless final Interpretation to `Validated` ```swift ValidationRules<ValidatedPartial<ValidationError>> .makeForm( firstName: "Tomás", lastName: "Ruiz-López", birthday: Date(timeIntervalSince1970: 10), documentId: "12345678T", phoneNumber: "666111222", email: "tomas.ruiz@47deg.com") ```
@tomasruizlopez | #AltConfMadrid | @bow_swift