🤝 swift-enum-properties
Struct and enum data access in harmony.
Motivation
In Swift, struct data access is far more ergonomic than enum data access by default.
A struct field can be accessed in less than a single line using expressive dot-syntax:
user.name
An enum's associated value requires as many as seven lines to bring it into the current scope:
let optionalValue: String?
if case let .success(value) = result {
optionalValue = value
} else {
optionalValue = nil
}
optionalValue
That's a lot of boilerplate getting in the way of what we care about: getting at the value of a success
.
This difference is also noticeable when working with higher-order functions like map
and compactMap
.
An array of struct values can be transformed succinctly in a single expression:
users.map { $0.name }
But transforming an array of enum values requires a version of the following incantation:
results.compactMap { result -> String? in
guard case let .success(value) = result else { return nil }
return value
}
The imperative nature of unwrapping an associated value spills over multiple lines, which requires us to give Swift an explicit return type, name our closure argument, and provide two explicit return
s.
Solution
We can recover all of the ergonomics of struct data access for enums by defining "enum properties": computed properties that optionally return a value when the case matches:
extension Result {
var success: Success? {
guard case let .success(value) = self else { return nil }
return value
}
var failure: Failure? {
guard case let .failure(value) = self else { return nil }
return value
}
}
This is work we are used to doing in an ad hoc way throughout our code bases, but we can centralize it in a computed property and are free to access underlying data in a succinct fashion:
// Optionally-chain into a successful result.
result.success?.count
// Collect a bunch of successful values.
results.compactMap { $0.success }
By defining a computed property, we bridge another gap: our enums now have key paths!
\Result<String, Error>.success
// KeyPath<Result<String, Error>, String?>
Despite these benefits, defining enum properties from scratch is a tall ask. Instead, enter generate-enum-properties
.
Usage
usage: generate-enum-properties [--help|-h] [--dry-run|-n] [<file>...]
-h, --help
Print this message.
-n, --dry-run
Don't update files in place. Print to stdout instead.
--version
Print the version.
Once installed, you can invoke generate-enum-properties
from the command line and feed it any number of Swift source files:
# Insert enum properties into every enum declaration.
$ generate-enum-properties **/*.swift
It will automatically generate and inline enum properties for every enum with associated values. Please note that it updates source files in place. Use version control to avoid accidental insertions! You can use the --dry-run
flag to preview the updated source.
$ generate-enum-properties --dry-run **/*.swift
Without the --dry-run
flag, the following source file as input:
enum Validated<Valid, Invalid> {
case valid(Valid)
case invalid(Invalid)
}
Will have its contents replaced with the following output:
enum Validated<Valid, Invalid> {
case valid(Valid)
case invalid(Invalid)
var valid: Valid? {
get {
guard case let .valid(value) = self else { return nil }
return value
}
set {
guard case .valid = self, let newValue = newValue else { return }
self = .valid(newValue)
}
}
}
Note that both a setter and getter are generated, which means you can also optionally dive into enum data and update a part of it.
validatedUser.valid?.name = "Blob"
Running generate-enum-properties
is idempotent: it will only insert properties that aren't already defined in the enum declaration. One caveat:
⚠️ If you have defined an enum property of the same name defined in an extension, it will collide with the one generated bygenerate-enum-properties
.
Now you may be wondering: why not generate extensions that can be hidden away in another file? Unfortunately, this is problematic for enums that depend on types that need to be imported and types that are nested. By inlining enum properties, we can ensure that every associated value's type is in scope.
Installation
Homebrew
You can install generate-enum-properties
using our custom tap:
$ brew install pointfreeco/swift/generate-enum-properties
$ generate-enum-properties
SwiftPM
As a dependency
If you want to use generate-enum-properties
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-enum-properties.git", from: "0.1.0")
]
And invoking swift run
from the command line:
$ swift run generate-enum-properties
As a CLI
If you want to run generate-enum-properties
using SwiftPM, it's as simple as cloning the repository and invoking swift run
:
$ git clone https://github.com/pointfreeco/swift-enum-properties.git
$ cd swift-enum-properties
$ swift run generate-enum-properties
Make
If you want to build and install generate-enum-properties
yourself:
$ git clone https://github.com/pointfreeco/swift-enum-properties.git
$ cd swift-enum-properties
$ make install
Mint
If you want to install with Mint:
$ mint install pointfreeco/swift-enum-properties
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.
The design of this library was explored in the following Point-Free episodes:
- Episode 52: Enum Properties
- Episode 53: Swift Syntax Enum Properties
- Episode 54: Advanced Swift Syntax Enum Properties
- Episode 55: Swift Syntax Command Line Tool
🆓
License
All modules are released under the MIT license. See LICENSE for details.