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.
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.
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.
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
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.
Here we’ve combined a few throwing functions into a single throwing function that may return a
User
.If the
id
,email
, orname
are invalid, an error is thrown.Unfortunately, if several or all of these inputs are invalid, the first error wins.
Handling multiple errors with Validated
Validated
is aResult
-like type that can accumulate multiple errors. Instead of usingthrow
ing functions, we can define functions that work withValidated
.To accumulate errors, we use a function that we may already be familiar with:
zip
.The
zip
function onValidated
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 singleValidated
values into singleValidated
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 newValidated
value with its valid case transformed.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 andValidated
inputs.An invalid input yields an error in the
invalid
case.More importantly, multiple invalid inputs yield an
invalid
case with multiple errors.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.
If you want to use Validated in a SwiftPM project, it’s as simple as adding it to a
dependencies
clause in yourPackage.swift
: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:
License
All modules are released under the MIT license. See LICENSE for details.