A library for treating predicates as first-class data types.
Use swift-predicate
with Swift Package Manager, by adding it as a dependency to your Package.swift
configuration:
dependencies: [
.package(url: "https://github.com/cdmcmahon/swift-predicate.git", from: "0.1.0")
]
Programs written in swift make frequent use of predicates. For example, they are commonly used to filter collections:
let evenNumbers = [1,2,3,4,5,6,7,8,9,10].filter { $0 % 2 == 0 }
However, while functions have inherent composability, they sometimes face ergonomic challenges in common Swift code. For example, to apply more complex filters, Swift programmers face the following options.
// Not very performant!
let hasEvenCharacterCount = ["foo", "bar", "foobar"]
.map { $0.count }
.filter { $0 % 2 == 0 }
// Not very reusable!
let hasEvenCharacterCount = ["foo", "bar", "foobar"]
.filter { $0.count % 2 == 0 }
// Good but not quite great!
let isEven = { $0 % 2 == 0 }
let hasEvenCharacterCount = ["foo", "bar", "foobar"]
.filter { isEven($0.count) }
That's where a first class data type for predicates comes in. Predicate
is a data type that wraps a predicate function and extends it with capabilities for composition. This library provides that type as well as extensions to many native swift types so that they can work well with a Predicate
.
let isEven = Predicate<Int> { $0 % 2 == 0 }
let isEmpty = Predicate<String> { $0.isEmpty }
struct User {
var email: String
var password: String
}
let hasSetUpProfile = Predicate<User> { user in
return !user.email.isEmpty && !user.password.isEmpty
}
Predicate
is designed to be easily composable, so that larger, more complicated predicates can be created from small, simple, reusable ones. For example, Predicate
provides nice ergonomics to modify a Predicate
or combine a Predicate<T>
with another of its same type:
let isEven = Predicate<Int> { $0 % 2 == 0 }
let isOdd = isEven.negate()
let isFoo = Predicate.equalTo("foo")
let isEvenAndGreaterThanTen = isEven.and { $0 > 10 }
let isOddOrGreaterThanTen = isOdd.or { $0 > 10 }
let isEvenXorGreaterThanTen = isEven.xor { $0 > 10 }
let allNumbers = isEven.or(isOdd)
// Bonus mode: isGreaterThanTen could be made reusable
These can also be written with static methods, if needed.
let allNumbers = Predicate.or(isEven, isOdd)
Additionally, pullback
(a.k.a. contramap
) can let us transform a predicates on Int
types to String
types, so long as we define a transformation from String
to Int
. In this case, we do that with character count:
let isEven = Predicate<Int> { $0 % 2 == 0 }
let hasEvenCharacterCount = ["foo", "bar", "foobar"]
.filter(isEven.pullback { $0.count })
The Predicate type is made to play well with collections.
This library was created with heavy inspiration from pointfree.co. The abstraction of a function interface into a data type follows a pattern frequently used in their video series. (Code samples here.)
Inspiration was also taken from the Predicate
functional interface in java.util.function
and the ergonomics were often designed to mimic that library.