Value integrity in Swift

Reaching the limits of statically typed systems Photo by Saad Ahmad on Unsplash Introduction I recently read the book Domain modeling made functional by Scott Wlaschin. Great book, highly recommend it. There is a whole chapter dedicated to integrity and consistency inside business domains, in the context of Domain-Driven Design. Scott defines Integrity as making sure that […]

Reaching the limits of statically typed systems

Photo by Saad Ahmad on Unsplash

Introduction

I recently read the book Domain modeling made functional by Scott Wlaschin. Great book, highly recommend it. There is a whole chapter dedicated to integrity and consistency inside business domains, in the context of Domain-Driven Design. Scott defines Integrity as making sure that a piece of data follows the correct business rules. In the context of an application like Jobandtalent, those business rules could be:

  • A user must have a valid user name, whose length is between 5 and 20 characters.
  • A user must have a validated phone number before applying for any job.

Let’s analyze different alternatives we have to ensure value integrity with Swift.

Enforcing integrity via types

The best way to enforce integrity and assure correct invariants of our values is by constraining our API, in a way that is simply impossible to have inconsistent data. That’s a bold statement, but good, expressive type systems should allow developers to convey those constraints in a way that the compiler prevents us from writing incorrect code in the first place. Swift introduced the concept of enum with associated values, which is usually referred to as sum types. In terms of API design, and coming from a C and Objective-C background, sum types have had the most impact on the way I design my code. I still remember my old Objective-C days, where we had to pass errors by reference (NSError **) and check for the success return value before reading the error content. Some other times, we had the tuple (data, error) as the input of our callback function, with illegal combinations like (nil, nil) or (data != nil, error != nil). Luckily, Swift brought better ways to deal with this via the Result<T, E> type and switch exhaustive checking of the different variants. Sum types are one of the best ways to enforce integrity in our domain.

The problem with Bool values

Another good usage of sum types is when we have booleans values in our domain. Let’s imagine that we want to model the phone number of a user.

struct PhoneNumber {
let phoneNumber: String
let isVerified: Bool
}

Whenever we see a Bool field in our values, we should ask ourselves:

  1. Do the rest of the fields make sense both for isVerified = true and isVerified = false? If we need to add some fields that only make sense when isVerified = true, that’s a clear sign that we need a better design.
  2. Will this value be used in contexts where only isVerified = true or isVerified = false make sense? That’s another clear sign that the design is not the correct one.

The problem with Bool values is the same as the issue we previously had with the tuple (data, error). It’s all about making sure that all the possible combinations of their fields make sense for all the usages of the type.

The alternative, as you might expect, is to use a sum type:

enum PhoneNumber {
case unverified(UnverifiedPhoneNumber)
case verified(VerifiedPhoneNumber)
}

Having different types for the verified and unverified phone numbers means that we could simply have the VerifiedPhoneNumber type in contexts where only isVerified = true makes sense. In fact, at the beginning of the article, we had this business rule:

A user must have a validated phone number before applying for any job.

A naive implementation would look like this.

struct Job {
let applicants: [User]
}
struct User {
let phoneNumber: PhoneNumber
}

When having this design, I wouldn’t be surprised to find asserts here and there forcing the different preconditions that we expect upon our model, that is, assert(user[index].phoneNumber.isVerified). The problem is that the compiler is not helping here and we might find the issue at runtime. By slightly changing the design to something like the following, the compiler won’t allow us to have that problem in the first place.

struct Job {
let applicants: [VerifiedUser]
}
struct VerifiedUser {
let phoneNumber: VerifiedPhoneNumber
}

The problem with UIKit and local reasoning

There are some other constraints we would like to impose in our design that, given the frameworks of choice (UIKit in this case), are simply not allowed or not convenient, forcing us to fall back to runtime assertions.

Let’s imagine we implement a button with UIKit, whose action callback expects the state in a very specific shape.

enum LoadingState {
case loading
case loaded(Data)
}
// The button is disabled when the state is loading, so it is 
// "guaranteed" that this function will only be called when the
// state is .loaded.

@IBAction private func buttonTapped() {
switch state {
case .loading:
preconditionFailure("Cannot happen")

case .loaded(let data):
// Do something with data
}
}
func render() {
switch state {
case .loading:
button.isEnabled = false

case .loaded(let data):
button.isEnabled = true
}
}

Based on the fact that the button will only be enabled when state = .loaded, we can be sure that the preconditionFailure won’t ever be executed. That’s certainly true for that code, but who knows if a future render refactor will change that, breaking thebuttonTapped logic. That’s a big problem. We cannot analyze the functions in isolation. We’ve lost local reasoning, which is one of the most important things in programming, as the buttonTapped action is coupled with the render method.

Fixing this problem requires ditching the target-selector pattern, so we can set a tapHandler closure every time the view is rendered, in the right loaded context.

func render() {
switch state {
case .loading:
button.isEnabled = false
    case .loaded(let data):
button.isEnabled = true
button.tapHandler = {
// Do something with data.
}

}
}

Dependent types

We are not so lucky sometimes… In a similar fashion to the Result type, the Validated type accumulates all the different validations failures that might arise.

enum Validated<T, E: Error> {
case valid(T)
case invalid([E])
}

In this case, when processing the invalid case, we know there must be one or more errors, but the compiler can’t enforce that. So we might add some kind of assert or similar not to mask and bury some possible bugs in our logic.

Non-empty lists are another quite common example. There are languages like Haskell that implement them in the standard library. In Swift, we could easily implement it like this:

struct NonEmptyList<A> {
let first: A
let rest: [A]
}

Or via a recursive sum type:

indirect enum NonEmptyList<A> {
case head(A)
case rest(A, Self)
}

So the correct Validated type would look like this:

enum Validated<T, E: Error> {
case valid(T)
case invalid(NonEmptyList<E>)
}

There are several problems with these implementations though.

In terms of ergonomics, we can’t use some of the commonly usedSequence methods, so we’d have to conform to those protocols manually.

And even with that, we’d still lose the performance optimizations that Swift does for us under the hood.

There are some good implementations of non-empty lists though, in case you want to take a look.

The same applies when constraining integer values. Swift doesn’t have a built-in way to represent natural numbers (unlike Rust), but you could easily build it yourself.

indirect enum Nat {
case one
case successor(Self)
}
let four: Nat = .successor(.successor(.successor(.one)))

Again, the same comment about ergonomics and performance applies.

This is where the workarounds to enforce integrity via types might not be worth it (or even possible). Who knows if we’ll have something like dependent types (in a similar fashion to Idris or Agda) in future Swift versions.

Property wrappers

Let’s go back to the other business rule at the beginning of the article.

A user must have a valid user name, whose length is between 5 and 20 characters.

A good way to ensure that, outside the statically-typed world of the compiler, is by using property wrappers.

struct Validation<Value> {
let predicate: (Value) -> Bool
}
// Having types wrapping non-nominal types (like function types) 
// lets us create nice APIs by extending the type and adding
// proper constructors.
extension Validation where Value == String { 
static func range(_ range: ClosedRange<Int>) -> Self {
.init { range ~= $0.count }
}
}
@propertyWrapper
struct Validated<A> {
var value: A? = nil
var validation: Validation<A>
  var wrappedValue: A? {
get {
value.flatMap { validation.predicate($0) ? $0 : nil }
}
set {
value = newValue
}
}
}
struct User {
@Validated(
value: "luisrecuenco",
validation: .range(5...20)
)
var username: String?
}

By doing so, we are enforcing that, as long as the property wrapper performs the correct logic, username will always be nil if the value is outside the given range.

But… what does a User with username = nil mean? Can that even happen? Or should it? By that design, it certainly can. But that’s a bit strange… A user should only be valid when the username is valid. In this case, the value of a user cannot guarantee its validity, so we need to add something that lets us know.

struct User {
@Validated(
value: "luisrecuenco",
validation: .range(5...20)
)
var username: String?
  var isValid: Bool {
username != nil
}

}

The problem is that, whenever we add new fields to the User struct, we may need to update the isValid implementation. And as we previously mentioned, whenever we have Bool properties, we need to ask ourselves if the value will be used in contexts where only one bool variant is correct. In this case, if the user is used in a context where we can only have valid users, then, we may need to add some asserts here and there to enforce our preconditions. As always, there’s no better way to enforce those preconditions than doing so via the compiler.

Failable and throwable initializers

So… as cool as property wrappers are, they might not be the right tool to solve this problem. How about leveraging failable initializers?

struct Username: RawRepresentable {
let rawValue: String
  init?(rawValue: String) {
guard 5...20 ~= rawValue.count else { return nil }

self.rawValue = rawValue
}
}
struct User {
var username: Username
}

Small but profound change. Not only have we given a proper name to something meaningful in our domain (a Username), but by moving the precondition to the initializer, we are forcing that Username can only be created given the correct underlying raw value. That also means that we can be sure that all User values have correct user names. Injecting a User value in different contexts will always mean a valid user. We’ve scoped the runtime check to the Username initializer, freeing the rest of our code from more assertions.

Instead of a failable initializer, we could as well have used a throwable initializer in the same way, which allows a more semantic error than nil.

Ergonomics

Whenever we lift unbounded types like Strings or Integers in a type, we lose ergonomics. Fortunately, Swift has great ways to recover that power. In the case of Username, we can conform to ExpressibleByStringLiteral, so we can create values via string literals.

extension Username: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(rawValue: value)!
}

}

We lose some type safety (that force unwrap!), but it can be very handy for test purposes to create Username values easily and conveniently.

Refinement types

The generalization of the previous concept is what’s usually known as refinement types, which could be defined as simple types, holding an underlying RawValue, with a (RawValue) -> Bool predicate.

How could we build that in Swift?

What we want is a generic type that can only be created when the underlying raw value meets the criteria defined by Precidate.

Refined<PhantomType, Predicate>

The phantom type will allow us to differentiate between different Refined types that use the very same predicate. As for the Predicate type, the previous Validation type is not enough, as a Validation<T> represents all possible validations for the type T. We need a different type that represents a concrete validation. One possible solution could be to have a Refinement protocol that uses the previous Validation type.

protocol Refinement {
associatedtype RawType
static var validation: Validation<RawType> { get }
}

With that in place, the final Refined type looks like this.

struct Refined<T, R> where R: Refinement {
let rawValue: R.RawType

init?(rawValue: R.RawType) {
guard R.validation.predicate(rawValue) else { return nil }
    self.rawValue = rawValue
}
}

And we could create a user name like follows:

struct UsernameRefinement: Refinement {
static let validation: Validation<String> = .range(5...20)
}
enum UsernameTag {}
typealias Username = Refined<UsernameTag, UsernameRefinement>
Username(rawValue: "luisrecuenco") // valid
Username(rawValue: "luis") // nil

Conclusion

Thanks a lot for reading. Before finishing, I’d like to talk about two important concepts, closely related to types.

  1. The rule of representation.
  2. The Curry–Howard correspondence.

Unix design is well-known for its minimalist and modular architecture. A lot of small little programs, doing one thing very well that can be easily composed to achieve bigger and more complex programs (a lot like Lego bricks). There are several principles behind this design. One of them is called the Rule of Representation:

Fold knowledge into data so program logic can be stupid and robust.

Types are one of the most powerful programming tools we have at our disposal as they can encapsulate, in a declarative manner, complex logic that, otherwise, would be scattered all around our program (those runtime checks we’ve been talking about again and again).

By the way, chapter 9 of The Mythical Man-Month stated this already in 1975.

Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowchart; it’ll be obvious

The last thing I’d like to talk about is the Curry–Howard correspondence, which relates mathematical proof and computer programs. At a very high level, we can have a correspondence between the math world and our programming world, where some logical propositions can be directly translated into types.

Seeing our types as propositions and our programs as proofs, closing the gap between math and programming, will definitely make our code more correct. But beware that more correct doesn’t necessarily mean better. There’s always a trade-off between convenient code and correct code. Closing that gap depends on us, but also on the languages we choose to use. Fortunately, Swift and a lot of other modern programming languages with great, expressive type systems, are making it easier for us to write code that’s correct, convenient, and understandable. We shouldn’t need to choose.

We are hiring!

body[data-twttr-rendered=”true”] {background-color: transparent;}.twitter-tweet {margin: auto !important;}

function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height);resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === “#amp=1” && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: “amp”, type: “embed-size”, height: height}, “*”);}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind(‘rendered’, function (event) {notifyResize();}); twttr.events.bind(‘resize’, function (event) {notifyResize();});if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute(“width”)); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}

If you want to know more about how it’s like work at Jobandtalent, you can read the first impressions of some of our teammates in this blog post or visit our Twitter.


Value integrity in Swift was originally published in Jobandtalent Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source: Jobandtalent