Can I specify that an associatedtype is a function?

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!)

1 Like

Surprisingly I get this error:

@escaping attribute only applies to function types

even though in the extension it's clear that F is a function type.

It's weird because as I said I can duplicate my initializer in different extensions like this, and the compiler accepts it:

struct Container<Call: CallType> {
  let description: String
  let block: Call.Callback
}

extension Container where Call == SyncCall {
  // no problem with @escaping:
  init(_ description: String, executing block: @escaping Call.Callback) {
    self.description = description
    self.block = block
  }
}

extension Container where Call == AsyncCall {
  init(_ description: String, executing block: @escaping Call.Callback) {
    self.description = description
    self.block = block
  }
}

@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.

6 Likes

Why do you think you need to do what you're asking? Can you provide more context?

struct Container<Closure> {
  let _callAsFunction: Closure

  func callAsFunction<each Parameter, Error, Return>(
    _ parameter: repeat each Parameter
  ) throws(Error) -> Return
  where Closure == @Sendable ((repeat each Parameter)) throws(Error) -> Return {
    try _callAsFunction((repeat each parameter))
  }

  func callAsFunction<each Parameter, Return>(
    _ parameter: repeat each Parameter
  ) async -> Return
  where Closure == @Sendable ((repeat each Parameter)) async -> Return {
    await _callAsFunction((repeat each parameter))
  }

  func callAsFunction<each Parameter, Return>(
    _ parameter: repeat each Parameter
  ) async throws -> Return
  where Closure == @Sendable ((repeat each Parameter)) async throws -> Return {
    try await _callAsFunction((repeat each parameter))
  }
}
let function1 = Container { @Sendable (_: ()) in 1 }
#expect(function1() == 1)

let function2 = Container { @Sendable (_: ()) in await Task { 1 }.value }
#expect(await function2() == 1)

let function3 = Container { @Sendable (_: ()) async throws in
  await Task { 1 }.value
}
#expect(try await function3() == 1)

Note: trying to use async + typed errors, and no explicit input type for this, is broken. The signature you actually want is

func callAsFunction<each Parameter, Error, Return>(
  _ parameter: repeat each Parameter
) async throws(Error) -> Return
where Closure == @Sendable (repeat each Parameter) async throws(Error) -> Return {
  try await _callAsFunction(repeat each parameter)
}

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?

Only because of bugs. To provide clearer and simplified context for what I said above, given this,

struct Container<Closure> {
  let _callAsFunction: Closure

  func callAsFunction<Error>() async throws(Error) -> Int
  where Closure == @Sendable () async throws(Error) -> Int {
    try await _callAsFunction()
  }
}

…This will work fine:

let function3 = Container { @Sendable () async throws in
  await Task { 1 }.value
}
#expect(try await function3() == 1)

…but this will hang forever:

let function2 = Container { @Sendable in await Task { 1 }.value }
#expect(await function2() == 1)

If I modify my code above to take out @escaping, then I get an error that block is non-escaping.

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.

I don't believe either the compiler or Swift Testing would introduce a hang here. The two examples appear to be equivalent.

It doesn't make sense to believe or not when you can verify the objective truth with a little copy/paste.

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()
1 Like

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?

What requirements would such a protocol declare?

1 Like

For my purposes at least, I just need it to express that it's a function type, so that I can use it with @escaping.

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
  }
}
1 Like

You don’t need to write @escaping here. It doesn’t mean anything when applied to a type parameter.

And yet the compiler complains:

Passing non-escaping parameter 'block' to function expecting an @escaping closure