Validated is a μ-library (~50 Source Lines of Code) that allows you make better use of Swift's type system by providing tools for easily generating new types with built-in guarantees.
Validated allows you to use the type system to verify properties of your values, providing a new level of compile time guarantees.
Using validators you can define new types that add guarantees to existing types:
// Create a new string type that can never be empty
typealias NonEmptyString = Validated<String, NonEmptyStringValidator>
You might have a function in your code that only knows how to work with a User
value when the user is logged in. Usually you will implement this requirement in code & add documentation, but you don't have an easy way of expressing this invariant in the type signature:
/// Please ever only call with a logged-in user!
func performTaskWithUser(user: User) {
precondition(
user.loggedIn,
"It is illegal to call this method with a logged out user!"
)
// ...
}
Using Validated you can quickly create a new type that describes this requirement in the type system. That makes it impossible to call the function with a logged-out user and it makes the method signature express your invariant (instead of relying on documentation):
func performTaskWithUser(user: LoggedInUser) {
// ...
}
So how is this new LoggedInUser
type created?
First, you need to implement a validator:
struct LoggedInValidator: Validator {
static func validate(value: User) -> Bool {
return value.loggedIn
}
}
A Validator
needs to implement the validate
function that takes the type that this validator can validate (in this case a User
). The funtion returns a Bool
. Return true
if the requirements are fulfilled and false
if not.
With the Validator
in place we can create our new type like this:
typealias LoggedInUser = Validated<User, LoggedInValidator>
Note, that it is not required to provide a typealias, but for most cases it is recommended.
And that's it!
LoggedInUser
now has a failable initializer that takes a User
. If the passed in User
fulfills the logged-in requirement you will have a LoggedInUser
, otherwise nil
. Additionally LoggedInUser
provides a throwing initializer, in case you prefer to handle failed validations as errors instead of nil
values.
The underlying value (the full User
value) is stored in the .value
property of LoggedInUser
.
Validated provides some further features that might be non-obvious.
Validated offers Validator
types for logical operations that allow you to require multiple validations in different combinations. E.g. you can use the And
validator to require that two requirements must be met for your type to intializer:
typealias AllCapsNonEmptyString =
Validated<String, And<NonEmptyStringValidator, AllCapsLatinStringValidator>>
Or
and Not
are provided as additional validators. You can take a look at the specs for additional examples.
A Validator
can itself be generic. This is useful if you want to provide verifications for a whole category of types. The example validator NonEmptyCollectionValidator
can be applied to all validator types by using a generic requirement:
struct NonEmptyCollectionValidator<T: CollectionType>: Validator {
static func validate(value: T) -> Bool {
if !value.isEmpty {
return true
} else {
return false
}
}
}
However, when using this validator to create a type, you will have to specify the exact type of collection you want to validate:
typealias NonEmptyListOfStrings = Validated<[String], NonEmptyCollectionValidator<[String]>>
No, not really. Dependent types would allow us to define types, solely based on values. This library only allows us to validate if a type is of a specified type <T>
based on its value and a validator. The value itself doesn't change the type information.
Truely dependent types would create the following results:
[1, 2, 3] // type = Array<count:3 type:Int>
[1, 2, 3, 4] // type = Array<count:4 type:Int>
Using Validated we can only verify if a type falls into one of our statically defined categories:
ListOf3([1,2,3]) // type = ListOf3<Array<Int>
ListOf3([1,2,3,4]) // type = nil
However, these statically provided checks can still add a lot of value to your code; see the examples above.
Validated is available via the usual suspects.
You can install Validated via CocoaPods by adding it to your Podfile
:
use_frameworks!
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
pod 'Validated'
And run pod install
.
You can install Validated via Carthage by adding the following line to your Cartfile:
github "Ben-G/Validated"
If you have any questions, you can find me on twitter @benjaminencz.