The Principal Dev – Masterclass for Tech Leads

The Principal Dev – Masterclass for Tech LeadsJuly 17-18

Join

🛂 Validated

CI

A result type that accumulates multiple errors.

Table of Contents

Motivation

The problem

Swift error handling short-circuits on the first failure. Because of this, it's not the greatest option for handling things like form data, where multiple inputs may result in multiple errors.

struct User {
  let id: Int
  let email: String
  let name: String
}

func validate(id: Int) throws -> Int {
  guard id > 0 else {
    throw Invalid.error("id must be greater than zero")
  }
  return id
}

func validate(email: String) throws -> String {
  guard email.contains("@") else {
    throw Invalid.error("email must be valid")
  }
  return email
}

func validate(name: String) throws -> String {
  guard !name.isEmpty else {
    throw Invalid.error("name can't be blank")
  }
  return name
}

func validateUser(id: Int, email: String, name: String) throws -> User {
  return User(
    id: try validate(id: id),
    email: try validate(id: email),
    name: try validate(id: name)
  )
}

Here we've combined a few throwing functions into a single throwing function that may return a User.

let user = try validateUser(id: 1, email: "blob@pointfree.co", name: "Blob")
// User(id: 1, email: "blob@pointfree.co", name: "Blob")

If the id, email, or name are invalid, an error is thrown.

let user = try validateUser(id: 1, email: "blob@pointfree.co", name: "")
// throws Invalid.error("name can't be blank")

Unfortunately, if several or all of these inputs are invalid, the first error wins.

let user = try validateUser(id: -1, email: "blobpointfree.co", name: "")
// throws Invalid.error("id must be greater than zero")

Handling multiple errors with Validated

Validated is a Result-like type that can accumulate multiple errors. Instead of using throwing functions, we can define functions that work with Validated.

func validate(id: Int) -> Validated<Int, String> {
  return id > 0
    ? .valid(id)
    : .error("id must be greater than zero")
}

func validate(email: String) -> Validated<String, String> {
  return email.contains("@")
    ? .valid(email)
    : .error("email must be valid")
}

func validate(name: String) -> Validated<String, String> {
  return !name.isEmpty
    ? .valid(name)
    : .error("name can't be blank")
}

To accumulate errors, we use a function that we may already be familiar with: zip.

let validInputs = zip(
  validate(id: 1),
  validate(email: "blob@pointfree.co"),
  validate(name: "Blob")
)
// Validated<(Int, String, String), String>

The zip function on Validated works much the same way it works on sequences, but rather than zipping a pair of sequences into a sequence of pairs, it zips up a group of single Validated values into single Validated value of a group.

From here, we can use another function that we may already be familiar with, map, which takes a transform function and produces a new Validated value with its valid case transformed.

let validUser = validInputs.map(User.init)
// valid(User(id: 1, email: "blob@pointfree.co", name: "Blob"))

Out group of valid inputs has transformed into a valid user.

For ergonomics and composition, a curried zip(with:) function is provided that takes both a transform function and Validated inputs.

zip(with: User.init)(
  validate(id: 1),
  validate(email: "blob@pointfree.co"),
  validate(name: "Blob")
)
// valid(User(id: 1, email: "blob@pointfree.co", name: "Blob"))

An invalid input yields an error in the invalid case.

zip(with: User.init)(
  validate(id: 1),
  validate(email: "blob@pointfree.co"),
  validate(name: "")
)
// invalid(["name can't be blank"])

More importantly, multiple invalid inputs yield an invalid case with multiple errors.

zip(with: User.init)(
  validate(id: -1),
  validate(email: "blobpointfree.co"),
  validate(name: "")
)
// invalid([
//   "id must be greater than zero",
//   "email must be valid",
//   "name can't be blank"
// ])

Invalid errors are held in a non-empty array to provide a compile-time guarantee that you will never encounter an empty invalid case.

Installation

You can add Validated to an Xcode project by adding it as a package dependency.

https://github.com/pointfreeco/swift-validated

If you want to use Validated in a SwiftPM project, it's as simple as adding it to a dependencies clause in your Package.swift:

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-validated", from: "0.2.1")
]

Interested in learning more?

These concepts (and more) are explored thoroughly in Point-Free, a video series exploring functional programming and Swift hosted by Brandon Williams and Stephen Celis.

Validated was explored in The Many Faces of Zip: Part 2:

video poster image

License

All modules are released under the MIT license. See LICENSE for details.

Join libs.tech

...and unlock some superpowers

GitHub

We won't share your data with anyone else.