Skip to content

CaptureContext/swift-equated

Repository files navigation

swift-equated

SwiftPM 6.2 Platforms @capture_context

Equatable wrapper type and a set of basic comparators.

Table of Contents

Motivation

Swift strongly encourages Equatable, and for good reason.
But in practice, equality often disappears at API boundaries:

  • values stored as any
  • errors erased to Swift.Error
  • closures, reference types, or foreign types
  • generic code that cannot add Equatable constraints

The problem

A very common example of this appears when using TCA, though the issue is by no means specific to it.

In TCA it’s idiomatic to make Actions equatable for better testing, diffing, and debugging. But the moment you want to carry an error, you hit a wall:

enum Action: Equatable {
  case requestFailed(Error) // ❌ 'Error' does not conform to 'Equatable'
}

A typical workaround is to introduce a bespoke wrapper type that forces equatability by comparing something like localizedDescription:

struct EquatableError: LocalizedError, Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool {
    lhs.localizedDescription == rhs.localizedDescription
  }

  let underlyingError: Error

  var localizedDescription: String {
    underlyingError.localizedDescription
  }
}

enum FeatureAction: Equatable {
  case requestFailed(EquatableError) // ✅ compiles
}

This works, but it comes with tradeoffs:

  • a new wrapper type must be declared
  • an equality strategy must be chosen and documented
  • the same pattern is often repeated across features and modules

And the “right” definition of equality is frequently context-dependent.

The solution

Equated lets you keep actions equatable without defining custom wrapper types:

import Equated

enum FeatureAction: Equatable {
  case requestFailed(Equated<Error>) // ✅ compiles
}

For errors, it’s enough to simply wrap the value:

return .send(.requestFailed(Equated(error)))

By default, Equated chooses an appropriate equality strategy:

  • if the underlying error can be compared using Equatable, == is used
  • otherwise it falls back to comparing localizedDescription

Note

Comparing localizedDescription is a heuristic. It is common, but not guaranteed to be unique or stable across localization changes.

Equality can always be customized explicitly when needed:

return .send(.requestFailed(Equated(error, by: .property(\.code))))

Features

Equated is a lightweight Equatable container that lets you define equality explicitly, while keeping call sites terse.

Predefined comparators

Choose how two values should be compared using a Equated.Comparator:

Automatic

The detectEquatable comparator attempts to cast values to any Equatable and compare them using ==:

.detectEquatable(
  checkBoth: Bool = false,
  fallback: Comparator = .dump
)

If equatable comparison is not possible, the provided fallback comparator is used.

Building blocks

  • .const(Bool) – always equal / never equal
  • .custom((Value, Value) -> Bool) – full control
  • .dump – compares the textual dump() output

Equatable-driven

  • .defaultEquatable – equivalent to using == directly

Property-based

The property comparator compares values by a derived equatable projection:

.property(\.someEquatableProperty)
.property { String(reflecting: $0) }
  • .objectID – compare reference identity (only when Value: AnyObject)

Error convenience

The .localizedDescription comparator is equivalent to .property(\.localizedDescription).

It is typically most useful as a fallback, for example:

.detectEquatable(fallback: .localizedDescription)

Concurrency escape hatches

  • .uncheckedSendable((Value) -> any Equatable)

    A property-style comparator for non-sendable projections

  • .uncheckedSendable((Value, Value) -> Bool)

    A custom-style comparator for non-sendable values

Note

Most users should prefer .detectEquatable() or .property comparators

Installation

Basic

You can add Equated to an Xcode project by adding it as a package dependency.

  1. From the File menu, select Swift Packages › Add Package Dependency…
  2. Enter "https://github.com/capturecontext/swift-equated" into the package repository URL text field
  3. Choose products you need to link them to your project.

Recommended

If you use SwiftPM for your project structure, add Equated to your package file.

.package(
  url: "git@github.com:capturecontext/swift-equated.git",
  .upToNextMinor(from: "0.0.1")
)

or via HTTPS

.package(
  url: "https://github.com:capturecontext/swift-equated.git",
  .upToNextMinor("0.0.1")
)

Do not forget about target dependencies:

.product(
  name: "Equated",
  package: "swift-equated"
)

License

This library is released under the MIT license. See LICENSE for details.

About

Equatable wrapper type and a set of basic comparators

Topics

Resources

License

Stars

Watchers

Forks

Languages