Posted on July 16, 2025 by ire

Struggling with abstraction and parametric polymorphism in Swift

I am currently writing a storage layer for an app, and it needs to support different backing storages, like UserDefaults, SQLite and an in-memory version for testing.

I have a very simple set of (self-imposed?) constraints:

  • Abstract
    • It should not leak its internals or have the external dependencies leaking in.
    • Call sites shouldn’t know how we are storing things and it shouldn’t know what it is storing/retrieving, etc.
  • Functional
    • Avoiding OOP pays off, we gain testability, composability, we remove a lot of boilerplate and ceremony, we remove hidden dependencies, hidden state mutations. The list goes on.
  • Testable
    • Ensuring the call sites remain testable, all without leaking any internals, in Swift that is not as easy.
  • A clear and nice API surface
    • What would be the point otherwise?

With that in mind, it was a tad more painful than I hoped for. I have complained about Swift having “half-baked” features and a mishmash of inconsistencies before, but this will not be a rant.

My initial approach resembled this AppStorage enum (it’s 2025 and Swift still has neither modules nor namespaces, so enums it is) holding a struct holding closures. This is the closest I could get to a Reader Monad ‘pattern’ without the complexity and boilerplate that using Monads would entail in Swift for very little benefit in a impure and referentially opaque language with very noisy syntax.

The idea is simple enough, we have a small record type holding functions. We can easily swap those functions with anything that matches their type signature without having to change any call sites. That means we can easily swap them for test fakes without boilerplate, such as single-use protocols, building whole object graphs to test one small thing.

enum AppStorageKeys {
  static let vibrationMode = "vibrationMode"
  static let vibrationFrequency = "vibrationFrequency"
  static let vibrationStrength = "vibrationStrength"
}

enum AppStorage {
  struct Functions {
    let getVibrationMode: () -> VibrationMode?
    let saveVibrationMode: (VibrationMode) -> Void
    let getVibrationFrequency: () -> VibrationFrequency?
    let saveVibrationFrequency: (VibrationFrequency) -> Void
    let getVibrationStrength: () -> VibrationStrength?
    let saveVibrationStrength: (VibrationStrength) -> Void
  }

  static func withDependencies(_ storageCore: StorageCore.Functions)
  -> Functions {
    Functions(
      getVibrationMode: {
        storageCore.load(key: AppStorageKeys.vibrationMode,
            type: VibrationMode.self)
      },
      saveVibrationMode: { mode in
        storageCore.store(key: AppStorageKeys.vibrationMode, 
            value: mode)
      },

      getVibrationFrequency: {
        storageCore.load(key: AppStorageKeys.vibrationFrequency, 
            type: VibrationFrequency.self)
      },
      saveVibrationFrequency: { frequency in
        storageCore.store(key: AppStorageKeys.vibrationFrequency,
            value: frequency)
      },

      getVibrationStrength: {
        storageCore.load(key: AppStorageKeys.vibrationStrength,
            type: VibrationStrength.self)
      },
      saveVibrationStrength: { strength in
        storageCore.store(key: AppStorageKeys.vibrationStrength,
            value: strength)
      },
    )
  }
}

The idea is simple enough but still involved some boilerplate. Namely making injection easy. Our next step in faking having a Reader stack is making our “environment” implicitly available.

Environment keys

With EnvironmentKey we are able to inject things into the “environment”, SwiftUI views are just “cheap” descriptors that live in this opaque “environment”, where we can add things like @Bindings, @States and @EnvironmentObject, but also simple @Environment values. In this case, since at least Swift has first class functions, we can store those!

struct AppStorageKey: EnvironmentKey {
  static var defaultValue: AppStorage.Functions = 
  AppStorage.withDependencies(
    StorageCore.withUserDefaults()
  )
}

extension EnvironmentValues {
  var appStorage: AppStorage.Functions {
    get { self[AppStorageKey.self] }
    set { self[AppStorageKey.self] = newValue }
  }
}

This allows us to add the following to our SwiftUI views:

struct SomeView: View {
  @Environment(\.appStorage) private var appStorage

Now we can call self.appStorage.store() from our view without ever leaking anything. We can also at any point override out default storage:

MainView(appConfig: self.appConfig, user: user)
  .environment(\.appStorage,
    AppStorage.withDependencies(
        storageCore.withSqlite() 
        // or .withInMemory()
    )
  )

You can see how easy it is. Now the problem becomes how to write StorageCore. I started with the same approach but quickly something became clear:

The only viable way I could find to keep the interface agnostic (load, store, remove) required using existential types (any Storable) which effectively makes all the protocol abstraction, including the default implementations that would allow us to write concise generic code with automatic conforming protocols, very hard if not impossible. What do I mean by that?

Take the following abstraction for storing and retrieving opaque types.

enum StorageError: Error {
  case decodingFailed
}

// This is meant as an abstract protocol
protocol Storable: Codable {
  static var table: SQLite.Table { get }

  static var primaryKeyColumn: SQLite.Expression<String> { get }
}

protocol TableStorable: Storable {
  static func load(key: String, from db: Connection) throws -> Self?

  func store(db: Connection) throws
}

protocol KVSStorable: TableStorable where Val == Data {
  associatedtype Val: Value where Val.Datatype : Equatable

  static var key: String { get }

  static var valueColumn: SQLite.Expression<Val> { get }

  static func decoded(from data: Data) throws -> Self

  func encoded() throws -> Val
}

extension KVSStorable {
  static var table: SQLite.Table {
    Table("key_value_store")
  }

  static var primaryKeyColumn: SQLite.Expression<String> {
    SQLite.Expression<String>("key")
  }

  static var valueColumn: SQLite.Expression<Data> {
    SQLite.Expression<Data>("value")
  }

  static func load(key: String, from db: Connection) throws -> Self? {
    guard
      let row = try db.pluck(Self.table.filter(Self.primaryKeyColumn == key))
    else {
      return nil
    }

    let data = row[Self.valueColumn]

    return try Self.decoded(from: data)
  }

  func store(db: Connection) throws {
    try db.run(
      Self.table.upsert(
        Self.primaryKeyColumn <- Self.key,
        Self.valueColumn <- self.encoded(),
        onConflictOf: Self.primaryKeyColumn
      )
    )
  }
}

// MARK: KVS Subtypes

protocol KVSStringStorable: KVSStorable, RawRepresentable 
    where RawValue == String { }

extension KVSStringStorable {
  static func decoded(from data: Data) throws -> Self {
    guard
      let string = String(data: data, encoding: .utf8),
      let instance = Self(rawValue: string) else
    {
      throw StorageError.decodingFailed
    }

    return instance
  }

  func encoded() throws -> Data {
    self.rawValue.data(using: .utf8)!
  }
}

protocol KVSInt64Storable: KVSStorable, RawRepresentable 
    where RawValue == UInt8 {}

extension KVSInt64Storable {
  static func decoded(from data: Data) throws -> Self {
    let uint8 = data.withUnsafeBytes { $0.load(as: UInt8.self) }

    guard
      let instance = Self(rawValue: uint8)
    else {
      throw StorageError.decodingFailed
    }
    return instance
  }

  func encoded() throws -> Data {
    withUnsafeBytes(of: self.rawValue) { Data($0) }
  }
}

protocol KVSDataStorable: KVSStorable where Val == Data {

}

extension KVSDataStorable {
  static func decoded(from data: Data) throws -> Self {
    let decoder = PropertyListDecoder()
    return try decoder.decode(Self.self, from: data)
  }

  func encoded() throws -> Data {
    let encoder = PropertyListEncoder()
    encoder.outputFormat = .binary
    return try encoder.encode(self)
  }
}

This seems pretty straight forward, we define a top level protocol with our interface, and then some sub protocols for more specific versions. By taking advantage of default implementations we can also make conforming to them very trivial for our types. Let’s see what that looks like:

extension VibrationMode: KVSStringStorable {
  static let key: String = "vibrationMode"
}

extension VibrationFrequency: KVSInt64Storable {
  static let key: String = "vibrationFrequency"
}

extension VibrationStrength: KVSInt64Storable {
  static let key: String = "vibrationStrength"
}

Fantastic, as long as they are KVS types and raw representable, we can conform to our KVS protocols with no effort. And for more complex types all we’d need to do and tell Swift how to store/retrieve it. For the purpose of this writing I am avoiding the more complex ‘table backed’ types that are not stored in a KVS table, but the same principle applies, with a bit more boilerplate for protocol conformance since Swift reflection is, to my knowledge, insufficient in allowing generic conformity like we can do for our key value store types. Perhaps a subject for future writings.

Now we’ve hit a problem. How do we actually implement our StorageCore?

enum StorageCore {
  struct Functions {
    let store: (String, any Storable) -> Void
    let load: (String, any Storable.Type) -> Any?
    let remove: (String, any Storable.Type) -> Void
  }

  static func withSqlite(path: String) -> Functions { }

  static func withUserDefaults() -> Functions { }

  static func withInMemory() -> Functions { }
}

There is no other way to write these closures than with opaque types. Either using existential types directly or a concrete AnyStorable for type erasure as we used to do before Swift added existential support. But the problem is that now there is an existential type where we need an universal one.

You can’t dispatch static methods to an existential, we don’t know what the concrete type is. There is no way to write a valid load function.

Swift does not provide us with enough parametric polymorphism to solve this. Whilst we do have that for value and reference types, their properties and methods, we can even do that with functions, there is no such facility for closures. There is no such thing as <A, T>(A) -> T? in Swift.

Interlude

At this point, it’s worth taking a step back and examining what is going on and what some of these terms mean.

What do we mean by existential type? Types come in many flavours, including existential types and its dual, universal types.

When we say a type is universal, we mean that a type T such that for all T property P applies (in Swift that usually means conforming to a protocol or something more complex like T: Value where Value.SubValue : Equatable but it can also just be T and we do not care what T is). We decide at the calling site what we are passing in and the implementation knows only the universal properties of our constrained type. If it’s equatable we can ==, if it’s RawRepresentable we can .rawValue and so on.

When we say it’s existential, we mean there exists some type T that has some property P, but we don’t know anything about it except that it exists. I can pass it around I can’t do much with it. Internally I might be using some concrete type C but I am exposing only some T, and the information that it was ever C is lost.

If this is confusing, maybe this helps:

// Universal
func load<T: Storable>(key: String) -> T

The caller knows what T is, but the implementation doesn’t.

// Existential
func load(key: String) -> any Storable

The implementation knows what the concrete type conforming to Storable is, but the caller doesn’t.

In short, with universal types the caller has a concrete type and the function has an opaque one. With existential types the implementation has a concrete type but the caller gets an opaque one.

Usually this is a Good Thing™. Having support for existential types is very nice when you want them. It’s not great when you need something else. Closures work well with existential types but not at all with universal ones.

// Another downside, can't name parameters.
let load: (_ key: String) -> (any Storable)? // valid swift

let load<T>: (_ key: String) -> T? // no such luck

Hopefully this helped clarify the problem. With that out of the way…

Back to our regular programming

If you have read or heard of Swift Evolution proposal 0253, you might have guessed where this was going.

In short, added in Swift 5.2 is the ability to imply a type as callable by implementing the callAsFunction method (no other signifier in place here). In practice that means any concrete type that implements this method can be treated as a function.

struct FakeFunc {
  func callAsFunction() -> String {
    "Fake Func!"
  }
}

let fake = FakeFunc()
print(fake()) // "Fake Func!"

With this proposal we brought into Swift Callable values of user-defined nominal types. What this means in practice? I can write the following protocols:

protocol StorageStorer {
  func callAsFunction(key: String, value: any Storable)
}

protocol StorageLoader {
  func callAsFunction<T: Storable>(key: String, type: T.Type) -> T?
}

protocol StorageRemover {
  func callAsFunction<T: Storable>(key: String, type: T.Type)
}

Add the following structs:

struct DBStorageLoader: StorageLoader {
  let db: Connection

  func callAsFunction<T: Storable>(key: String, type: T.Type) -> T? {
    do {
      return try type.load(key: key, from: db)
    } catch {
      log.error("Failed to load \(type) from db.")
      return nil
    }
  }
}

struct InMemoryStorageLoader: StorageLoader {
  let store: InMemoryStore

  func callAsFunction<T: Storable>(key: String, type: T.Type) -> T? {
    self.store.data(forKey: key) as? T
  }
}

/// etc

And now my Functions type changes to

enum StorageCore {
  struct Functions {
    let store: StorageStorer
    let load: StorageLoader
    let remove: StorageRemover
  }
}

And not only do we get our fake generic closure, we regain parameter names. And the call site, you ask? You’ve seen it already!

storageCore.store(key: AppStorageKeys.vibrationMode, value: mode)

And that’s it, we break away from the language limitations and hack our way into jamming some parametric polymorphism into our closures, with the benefit of named parameters. We can go back to work and start using our abstract storage via functional injection.

A closing rant

This will be an opinionated complaint about the state of Swift. You don’t need to read it, but I feel the need to write it.

Swift’s seeming ongoing philosophy of leaving half-baked features forever half-baked and just adding bloat and syntactic noise to the language strikes again. Instead of improving our polymorphism with, I don’t know, higher kinded types so I don’t have to reinvent map and fold and every other abstraction for every single new type I define, we get this Ruby-esque overloading method_missing level of “magical” syntax.

There is absolutely nothing in the code that signifies to the user at any point that those structures can be treated as “functions”, not even from the auto-complete, until you actually open the first ( it does not suggest it. All we can hope for is that someone coming in is familiar with all this and reads whatever documentation you’ve managed to provide yourself.

I would imagine that if someone unfamiliar with what is going on looks at the call site and jumps to the definition will be bewildered. Their first conclusion might even be that jump to definition had a bug and jumped to the wrong place. There are no functions being declared here. I have no idea how something like this happened, except it happens all the time.

It’s just frustrating.