FunctionalProtocols for Swift

I have thought about this too while trying to make closures conditionally Sendable and came to the same idea. Closures don't really participate in the generics system and therefore closures can't be conditionally Sendable today (or conform to any other protocol conditional). On the other hand, Protocols and structs/enums/classes play very nicely with the generics system but lack the convince described in this thread.

If we extend on this idea and combine it with other concepts this becomes even more powerful:

1. Safely capturing mutable variables in a Sendable closure

Some Sendable closures are guaranteed to never run concurrently but we can't express this today in the type system. Therefore it is never allowed that they capture any mutable state, even if it would be safe. However, protocols can express this through a mutating function which guarantees exclusive access and making it non-copyable gives the closure still reference semantics:

protocol SerialSendableFunction: ~Copyable, Sendable {
    mutating func callAsFunction()
}

// requirement: only the closure access this mutable variable
var counter: Int = 0
let closure: SerialFunction = {
    counter += 1
    print(counter)
}
/// would get translated to:
struct $SerialFunction: ~Copyable, SerialFunction {
    var counter: Int = 0
    mutating func callAsFunction() {
        counter += 1
        print(counter)
    }
}
let closure = $SerialFunction(counter: 0)

2. Inline storage for captured variables

As a side benefit of making it non-copyable we also reduce the allocation from one allocation for the closure context to zero because the variable is stored inline in the non-copyable struct. Closures have an optimization if nothing is captures or exactly one AnyObject but otherwise needs to allocate storage on the heap for them. (currently the compiler actually allocates each variable separately but this seems fixable)

This was also mentioned in the motivation of the Callable proposal.

In theory this should help the optimizer as well as it can now see through the closures more easily because they are now types and the compiler doesn't need to inline everything.

3. Closures with value semantics

Closures could in theory have value semantics if they are translated to a Copyable struct, even if capturing mutable variables. I think this would be very unintuitive at first and would probably require some special syntax at the call site to make it clear if a closure has value or reference semantics. However, I think it could still be very valuable to implement stateful but "restartable" operations. On example of this is a HTTPClient configuration for redirects e.g.:

protocol HTTPRedirectPolicy: Sendable /* implicit Copyable */ {
    mutating func callAsFunction(_ redirectURL: URL) -> Bool
}

var config = HTTPConfig()

var redirectCountPerRequest: Int = 0
// property is of type `any HTTPRedirectPolicy`
config.shouldFollowRedirect = { _ in {
   redirectCountPerRequest += 1
   return redirectCountPerRequest < 3
}
// converted to
struct $HTTPRedirectPolicy: Sendable /* implicit Copyable */ {
    var redirectCountPerRequest: Int
    mutating func callAsFunction(_ redirectURL: URL) -> Bool {
       redirectCountPerRequest += 1
       return redirectCountPerRequest < 3
    }
}
config.shouldFollowRedirect = $HTTPRedirectPolicy(redirectCountPerRequest: 0)

config.shouldFollowRedirect is now a template from which the HTTPClient creates a copy for each request. As I said earlier, value semantics would be rather unitive here as we are all used to reference semantics but nevertheless useful. Just an idea that builds on top of the rest that I expect to be the most controversial. Each individual feature would be valuable on its own so this doesn't need to be included for the rest to be useful.

Alternatives

Closures as Generic Type Constraints

Instead of this we could also make closures/functions participle in the generics system. In addition we would also need ~Copyable and mutating closures.

The other way around - Types as Closures

Once a defined closure grows in complexity or the same logic is used in multiple places, I often struggle to find a good place to put it. Being able to define a type that can be used at places where closures are used would be very nice as well! Types are a great place to put documentation. We could accept types that have a callAsFunction() method defined at places where closures are accepted. This almost works today by we need to add .callAsFunction to make it compile:

class Foo {
    var counter: Int
    init(counter: Int) {
        self.counter = counter
    }
    func callAsFunction() {
        counter += 1
        print(counter)
    }
}

func functionWithClosureArgument(_ function: () -> ()) {
    function()
}

// .callAsFunction required today
functionWithClosureArgument(Foo(counter: 0).callAsFunction)
// instead we could allow the type to be passed directly
functionWithClosureArgument(Foo(counter: 0))

This seems almost like a bug fix but I'm probably overlooking some overload or type inference ambiguities that would arise from this change.


Overall I think it would make perfect sense to bring closures and protocols closer together. ~Escapable is now also coming to protocols (and other types), a privilege that was previously reserved for closures. Ideally closures should just be syntax sugar.

3 Likes