I'm trying to create a container to hold a callback which may or may not be async to help avoid duplicating the logic that applies in either case.
Currently I'm storing it in an enum with associated values, but in many cases that forces me to treat it like too much of a "black box" because I know from the context which type it actually is, so I'm trying to find a more elegant solution.
I've started my new approach like this:
public protocol CallType: Sendable {
associatedtype Callback: Sendable // really it's a function!
}
public enum SyncCall: CallType {
public typealias Callback = @Sendable () throws -> Void
}
public enum AsyncCall: CallType {
public typealias Callback = @Sendable () async throws -> Void
}
struct Container<Call: CallType> {
let description: String
let block: Call.Callback
init(_ description: String, executing block: Call.Callback) {
self.description = description
self.block = block
}
}
The catch is I need the executing parameter to be @escaping but the compiler doesn't know that Callback is supposed to be a function type. Is there a way to express this?
I could declare async and non-async versions of the initializer in extensions, so the compiler would know about the concrete function type, but I want to avoid that code duplication, especially since I actually have multiple initializers.
Not directly as there is no "function" protocol to constrain against, but you can do:
struct FBox<F, each A, E, R> where E: Error {
private var f: F
}
extension FBox where F == @Sendable (repeat each A) throws(E) -> R {
init(_ f: @escaping F) { ... }
}
extension FBox where F == @Sendable (repeat each A) async throws(E) -> R {
init(_ f: @escaping F) { ... }
}
(Typed from my phone in a food court, so I have no idea if it'll compile!)
@escaping is unnecessary except when a function type is the immediate type of a function parameter. A function type in any other position is implicitly assumed to be @escaping.
Even if this could be expressed, there would be no way to call a function that is dynamically either sync or async, because their calling conventions are rather different. Since a sync function can be converted into an async function, do you really need both initializers?
I'm not trying to have the call work dynamically; I know that's not really doable. But there is some logic that I'm using in both async and non-async scenarios that I'm trying to avoid duplicating.
Specifically, I'm working on a testing library that gets used like this:
func testSomething()
{
spec { // this takes a result builder
beforeEach {
// initialization
}
it("does the thing") {
// verify
}
it("does another thing") {
// verify
}
}
}
func testAsyncThings() async
{
await spec {
it("does something asynchronously") {
// you can await stuff in here
}
}
}
I want to use the same logic (like running the beforeEach block for each test element) without having to duplicate it all in async and non-async versions of the test runner function.
That's fair enough. I should have tested it before I replied. It does appear to be hanging with the Swift 6.1 toolchain, although it doesn't appear #expect() has anything to do with it. I don't see an obvious root cause.
Any chance you filed an issue?
Edit: We can simplify the failure case to just:
let function2 = Container { @Sendable () async in
1
}
await function2()
So it sounds like the answer is no, there is no way to specify that an associatedtype is a function type.
Would it make sense to add a built-in protocol that all function types conform to, to make this possible? Or would this be considered too obscure of a use case?
This sounds like a misunderstanding unfortunately. Non-escaping function types are not “first-class” and they cannot be abstracted over by a type parameter in the language, so it is never necessary or even meaningful to apply @escaping to a type parameter.
As I mentioned above, what I have now is something like this:
public protocol CallType: Sendable {
associatedtype Callback: Sendable
}
public enum SyncCall: CallType {
public typealias Callback = @Sendable () throws -> Void
}
public enum AsyncCall: CallType {
public typealias Callback = @Sendable () async throws -> Void
}
struct Container<Call: CallType> {
let description: String
let block: Call.Callback
init(_ description: String, executing block: @escaping Call.Callback)
where Call == SyncCall {
self.description = description
self.block = block
}
init(_ description: String, executing block: @escaping Call.Callback)
where Call == AsyncCall {
self.description = description
self.block = block
}
}
@escaping is required there since I'm storing the callback, but I can only use it if the type I'm talking about is known by the compiler to be a function type. Thus I have to have two copies of the initializer, differing only in the where clause, so the compiler knows I'm talking about SyncCall.Callback and AsyncCall.Callback which are function types.
But if we had a built-in protocol for that - let's pretend it's called FunctionType - then I'm thinking I could do it this way:
public protocol CallType: Sendable {
associatedtype Callback: Sendable, FunctionType
}
// same SyncCall and AsyncCall as above
struct Container<Call: CallType> {
let description: String
let block: Call.Callback
// Just one version of the initializer because Call.Callback conforms
// to FunctionType and so @escaping can be applied.
init(_ description: String, executing block: @escaping Call.Callback) {
self.description = description
self.block = block
}
}