Slide 1

Slide 1 text

Error Management and Data Validation with Bow Tomás Ruiz-López Senior Software Engineer at 47degrees @tomasruizlopez @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 2

Slide 2 text

| @47deg 47deg.com @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Error handling @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Validation functions func validate(firstName: String) throws String @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 10

Slide 10 text

Using Optionals @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Encode information in types Use the compiler in your advantage @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 13

Slide 13 text

Using Result @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Using Result func validate(email: String) Result { if email.contains("@") { return .success(email) } else { return .failure(.invalidEmail(email)) } } @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Using Result func matches(_ string: String, regExp: NSRegularExpression, orElse: (String) ValidationError) Result { 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      

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

@tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 26

Slide 26 text

Either @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 27

Slide 27 text

Either data type Represents the sum of two types. Either.left ~ Result.failure Either.right ~ Result.success @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 28

Slide 28 text

Either data type Result can be easily transformed to Either and leverage its API: let firstNameResult: Result = validate(firstName: "Tomás") let firstNameEither: Either = firstNameResult.toEither() let backToResult = firstNameEither.toResult() @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 29

Slide 29 text

Either - Fail fast func failFast(firstName: String, lastName: String, birthday: Date, documentId: String, phoneNumber: String, email: String) Either { return Either.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      

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Validated @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 33

Slide 33 text

Validated data type A data type to represent valid and invalid values. Validated.valid ~ Result.success Validated.invalid ~ Result.failure @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 34

Slide 34 text

Validated data type Result can easily be transformed to Validated and leverage its API: let firstNameResult: Result = validate(firstName: "Tomás") let firstNameValidated: Validated = firstNameResult.toValidated() @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 35

Slide 35 text

Validated - Error accumulation It models having a single error: Validated @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Make illegal states impossible to represent Again, use the compiler in your advantage @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 38

Slide 38 text

Validated - Error accumulation Accumulates errors and guarantees there is at least one: Validated, Form> Or alternatively: Validated, Form> Even more succint, with the ValidatedNEA typealias: ValidatedNEA @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 39

Slide 39 text

Validated data type Result can easily be transformed to ValidatedNEA and leverage its API: let firstNameResult: Result = validate(firstName: "Tomás") let firstNameValidated: ValidatedNEA firstNameResult.toValidatedNEA() @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 40

Slide 40 text

Validated - Error accumulation func errorAccummulation(firstName: String, lastName: String, birthday: Date, documentId: String, phoneNumber: String, email: String) ValidatedNEA.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      

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Summary Data type Extract values Combine values Combination strategy Result Pattern matching Either fold map Fail-fast Validated fold map Error accumulation @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 44

Slide 44 text

Applicative @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 45

Slide 45 text

Applicative With simulated Higher-Kinded Types: Someday, with native Higher-Kinded Types... static func ap(_ ff: Kind, _ fa: Kind) Kind static func ap(_ ff: F<(A) B>, _ fa: F) F @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 46

Slide 46 text

Applicative Perform independent computations and combine their results. Either.map( fa, fb, fc) { a, b, c in } Validated.map( fa, fb, fc) { a, b, c in } In general, working with any type F F.map( fa, fb, fc) { a, b, c in } @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 47

Slide 47 text

Conclusions @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Thanks! Questions? Bow Tomás Ruiz-López: Bow: | 47degrees: | @tomasruizlopez @bow_swift bow-swift.io @47deg 47deg.com @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 50

Slide 50 text

Bonus Track I I don't care about the type that I'm using a.k.a. Tagless Final @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 51

Slide 51 text

Tagless Final F ApplicativeError F.pure ~ Result.success F.raiseError ~ Result.failure @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 52

Slide 52 text

Tagless Final Instances of ApplicativeError Data Type pure raiseError Either right left Validated valid invalid @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 53

Slide 53 text

Tagless Final Creating success and failure class ValidationRules where F.E NEA { static func validate(email: String) Kind { if email.contains("@") { return .pure(email) } else { return .raiseError(.of(.invalidEmail(email))) } } } @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 54

Slide 54 text

Tagless Final Combining independent computations class ValidationRules { static func makeForm(firstName: String, lastName: String, birthday: Date, documentId: String, phoneNumber: String, email: String) Kind { 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      

Slide 55

Slide 55 text

Tagless final Interpretation to Either ValidationRules

Slide 56

Slide 56 text

Tagless final Interpretation to Validated ValidationRules

Slide 57

Slide 57 text

Bonus Track II I don't need to use NonEmptyArray thanks to Semigroup @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Semigroup + Tagless Final Validation functions class ValidationRules where F.E ValidationError { static func validate(email: String) Kind { if email.contains("@") { return .pure(email) } else { return .raiseError(.invalidEmail(email)) } } } @tomasruizlopez | #AltConfMadrid | @bow_swift      

Slide 62

Slide 62 text

Semigroup + Tagless final Interpretation to Either ValidationRules

Slide 63

Slide 63 text

Semigroup + Tagless final Interpretation to Validated ValidationRules