33

GitHub - pointfreeco/swift-validated: ? A result type that accumulates multiple...

 5 years ago
source link: https://github.com/pointfreeco/swift-validated
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

README.md

? Validated

Swift 4.1 iOS/macOS CI Linux CI @pointfreeco

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) -> Int throws {
  guard id > 0 else {
    throw Invalid.error("id must be greater than zero")
  }
  return id
}

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

func validate(name: String) -> String throws {
  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: "[email protected]", name: "Blob")
// User(id: 1, email: "[email protected]", name: "Blob")

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

let user = try validateUser(id: 1, email: "[email protected]", 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: "[email protected]"),
  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: "[email protected]", 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: "[email protected]"),
  validate(name: "Blob")
)
// valid(User(id: 1, email: "[email protected]", name: "Blob"))

An invalid input yields an error in the invalid case.

zip(with: User.init)(
  validate(id: 1),
  validate(email: "[email protected]"),
  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: "[email protected]"),
  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

Carthage

If you use Carthage, you can add the following dependency to your Cartfile:

github "pointfreeco/swift-validated" ~> 0.1

CocoaPods

If your project uses CocoaPods, just add the following to your Podfile:

pod 'PointFree-Validated', '~> 0.1'

SwiftPM

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

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

Xcode Sub-project

Submodule, clone, or download Validated, and drag Validated.xcodeproj into your project.

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK