No way to preserve type attributes in generic closure-wrapping functions

There's no way to write a generic closure wrapper that preserves type attributes (@MainActor, @Sendable, custom global actors).

Example

@globalActor actor MyActor { static let shared = MyActor() }

func inject<T>(_ f: @escaping (Int) -> T) -> () -> T {
    { f(42) }
}

let f = inject { @MyActor x in print(x) }
// error: converting '@MyActor @Sendable (Int) -> ()'
//        to '(Int) -> ()' loses global actor 'MyActor'

The closure is @MyActor, but (Int) -> T can't carry that. Attribute lost.

To preserve it, you need a separate overload per attribute:

func inject<T>(_ f: @escaping @MyActor (Int) -> T) -> @MyActor () -> T { ... }

This is not a viable option - throws/async combinations already require 4 overloads, each attribute doubles that, and custom global actors defined by downstream users make it impossible. Moreover, you can't even have both @MainActor and @MainActor @Sendable overloads - the compiler considers them a redeclaration.

Discussion

Has there been any discussion about parameterizing over type attributes? Something like:

func inject<T, @attributes Attr>(_ f: @escaping @Attr (Int) -> T) -> @Attr () -> T

This seems like a natural next step after parameter packs solved the arity problem - type attributes are the remaining axis of the overload explosion. Curious if anyone has run into this or if there are existing proposals/pitches I missed.

For the case of actor isolation specifically, isn't this what @isolated(any) is for?

1 Like

As @bbrk24 mentioned, with @isolated(any) you can, at least, retain the isolation dynamically:

func inject<T: Sendable>(
	_ f: @escaping @isolated(any) (Int) -> T)
-> () async -> T {
	{ await f(42) }
}

// or

func inject<T>(
	_ f: @escaping @isolated(any) (Int) -> sending T)
-> () async -> T {
	{ await f(42) }
}

The proposal: @isolated(any) Function Types