I'm sure that this has been discussed, planned, maybe discarded or postponed even, but I can't come up with the proper terms to find this :)
The basic idea is:
- Given a
Foo<each T>
- How could we (eventually) express that the list of T's contains another type, like
extension Foo where (repeat each T).contains(ConcreteType.self) { ... }
?
Illustrating with an Example
Imagine you have a container of typed dependencies like so:
struct Context<each Dependency> {
let dependencies: (repeat each Dependency)
init(_ dependencies: repeat each Dependency) {
self.dependencies = (repeat each dependencies)
}
}
Then it'd be possible to put a couple of "dependencies" into a concrete context:
let appContext = Context(DB(), HTTP(), Logging())
This would fit into a logging function that uploads data to HTTP that was fetched from a DB like this:
func uploadUserName(
userID: UserID
) -> (Context<DB, HTTP, Logging>) async -> Bool
However, if you use a function to read the user name from the DB like this:
func getUserName(userID: UserID) -> (Context<DB>) -> String
... then there's no simple way to express on the type level that you can get from Context<DB, HTTP, Logging>
to Context<DB>
.
Note that it's currently (Swift 6.1) perfectly possible to convert one into the other at run time:
func uploadUserName(
userID: UserID
) -> (Context<DB, HTTP, Logging>) async -> Bool
return { context in
// Make a Context<DB> from the Context<DB,_,_>:
let name = getUserName(userID: userID)(Context(context.dependencies.0))
context.dependencies.2.log("-- Uploading user name")
do {
try await context.dependencies.1.upload(name)
context.dependencies.2.log("Upload finished successfully")
return true
} catch {
context.dependencies.2.log("Upload finished with error: \(error)")
return false
}
}
}
Getting to pack elements more nicely?
You'll notice that the positional tuple value access is a bit cumbersome.
context.dependencies.1.upload(name)
context.dependencies.2.log("Upload finished successfully")
You can't tell what 0, 1, 2 are without inferring from the method that's being called, or looking at the type declaration further to the top.
So you might want to reach for a typed accessor that allows:
context[HTTP.self].upload(name)
However, it appears that currently you end up with either
- a partial function (accepting wider input that in can handle, producing runtime errors)
- an optional or throwing function (expressing that you don't know at compile time)
Variant A: Partial function that throws
extension Context {
subscript<ADependency>(_ DepType: ADependency.Type) -> ADependency {
get {
for dependency in repeat each self.dependencies {
if let d = dependency as? ADependency {
return d
}
}
preconditionFailure("Dependency missing for type \(DepType.self)")
}
}
}
This allows you to write:
func uploadUserName(
userID: UserID
) -> (Context<DB, HTTP, Logging>) async -> Bool {
return { context in
let name = getUserName(userID: userID)(Context(context[DB.self]))
context[Logging.self].log("-- Uploading user name")
do {
try await context[HTTP.self].upload(name)
context[Logging.self].log("Upload finished successfully")
return true
} catch {
context[Logging.self].log("Upload finished with error: \(error)")
return false
}
}
}
But even though you know that Context<DB, HTTP, Logging>
contains a value of each of the types DB
, HTTP
, Logging
, this API permits shooting yourself in the foot:
func getUserName(userID: UserID) -> (Context<DB>) -> String {
return { context in
let name = context[DB.self].userName(userID: userID)
// Let me just also log this real quick ...
context[Logging.self].log("Found name \(name) for ID \(userID)")
// ⚠️ runtime error! Forgot to declare Context<DB, Logging>
return name
}
}
Variant B: Expressing uncertainty with optional or errors
So instead of a partial function, define subscript
to either return nil
, or express you expect this to work with a Result
/throwing an error:
extension Context {
struct DependencyMissing: Error { init() {} }
subscript<ADependency>(_ DepType: ADependency.Type) -> ADependency {
get throws {
for dependency in repeat each self.dependencies {
if let d = dependency as? ADependency {
return d
}
}
throw DependencyMissing()
}
}
}
But that will make all call-sites worse.
You know when you have access to the DB that getting to the DB will not fail. So unwrapping try?
or do-catch-ing is cumbersome.
But if you express your confidence with force try!
, you end up with a partial function again.
Solution: Don't use nice getters, stick to what the compiler knows
When we want to lean on the compiler, this seems to be the best we have:
context.dependencies.1.upload(name)
context.dependencies.2.log("Upload finished successfully")
You can't compile code that accesses context.dependencies.999
in a Context
with just 3 elements in the pack.
Yes, if you reorder types in the pack, you need to change the call site to reflect that.
But unless we can expose e.g. type containment like so:
// Purely fictional code:
extension Context where (repeat each Dependency).contains(DB.self) {
var db: DB {
for dependency in repeat each self.dependencies {
if let d = dependency as? DB {
return d
}
}
fatalError("Should not be reachable")
}
}
... I see no better way out.