[Pitch] Method capture lists (self / weak self / unowned self / none) for controlling implicit `self` in method bodies

Motivation

Swift closures have capture lists that let developers control how values are captured (e.g. weak self, unowned self, or binding captures like x = expr).

However, **instance methods ** don’t have an equivalent mechanism. In practice this becomes painful with long-running async work, especially when a task/loop outlives the last external reference to an actor or class instance.

A common pattern today is:


final actor WorkerActor {

    private var loopTask: Task<Void, Never>?

    func start() {
        loopTask = Task { [weak self] in
            await self?.runLoop()
        }
    }

    func runLoop() async {
        // Long-running / infinite loop that observes cancellation
    }

    deinit { loopTask?.cancel() }

}

Even though the task captures self weakly, once we call an **actor-isolated ** instance method, we effectively re-establish a dependency on the actor instance’s lifetime for the duration of that method’s work. This has led to repeated confusion and workarounds in real code bases.

Developers can work around this by moving logic into static functions and passing weak wrappers, but that harms ergonomics and often complicates actor isolation guarantees.

Proposed solution

Allow **methods (and functions) ** to declare a capture list—syntactically and semantically aligned with closure capture lists—to control how self is available inside the method body.

High-level goals

  1. Keep the **current default behavior **: implicit self works as today.

  2. Add an **opt-in ** way to make self availability explicit and/or weakened.

  3. Reuse the **existing capture-list features ** developers already know from closures, including binding captures like name = expr.

Strawman syntax options (pick one for discussion)

Option A (closest to closure capture list placement):


func f[weak self](x: Int) async { ... }

Option B (post-parameter clause):


func f(x: Int) [weak self] async { ... }

Option C (body signature clause, closure-like):


func f(x: Int) async { [weak self] in ... }

I currently prefer **Option A ** because it reads as “part of the function signature” without interfering with async/throws ordering, and it visually matches closure capture-list syntax.

Detailed design

1) Default equivalence

The following are equivalent:


func testFunc() { ... }

func testFunc[self]() { ... }

I.e. default is [self].

2) self capture modes

Inside the method body:

  • func f[self]() (or no capture list)

self behaves exactly as today.

  • func f[weak self]()

self is available as Self?, requiring optional chaining / unwrapping (mirrors closure behavior).

  • func f[unowned self]()

self is available as Self with the same safety characteristics as unowned in closures.

Explicit-capture mode: empty list ([]) and readable alias ([none])

When a method *has * a capture list, it enables an **explicit-capture mode **: self is **only ** available inside the method body if it is explicitly listed (as self, weak self, or unowned self).

To express “capture nothing (including no self)”, this pitch proposes two spellings that are **equivalent **:

  • func f[]()

  • func f[none]() *(readable alias for the empty list) *

Because [none] is just a spelling of the **empty ** list, it is **not combinable ** with other entries.

**Semantics (in the method body): ** if self is not captured, then:

  • The keyword self is unavailable.

  • Implicit member lookup through self is disabled (so property / method() are rejected).

  • Binding captures can still introduce specific names (see next section).

Example diagnostics (illustrative):


final class Example {

    var value: Int = 0

    func read[]() {
        _ = value // error: instance member 'value' is unavailable because `self` is not in scope
        _ = self.value // error: `self` is unavailable in an explicit-capture method unless captured
    }

    func read2[cached = value]() {
        _ = value // error: instance member 'value' is unavailable because `self` is not in scope
        _ = cached // ok
    }
}

Alternative design direction: opt-out-only ([no self])

As an alternative to the explicit-capture model above, Swift could keep today’s default (implicit self availability) even when other captures are present, and add a single opt-out spelling:

  • func f[no self]() — disables self and implicit member lookup.

This pitch treats **explicit-capture ( **[]** / **[none]** ) ** and **opt-out-only ( **[no self]** ) ** as mutually exclusive design directions.

3) Binding captures in method capture lists

Support binding captures identical in shape to closure capture lists, e.g.:


final class Example {

    var text: String = ""
    var count: Int = 0

    func work[cachedCount = text.count]() {
        // `self` is unavailable here, but `cachedCount` is available.
        // useful for avoiding implicit member lookup while still capturing specific values.
        _ = cachedCount
    }
}

This mirrors closure capture lists that bind names to expressions to capture snapshots.

**Evaluation time: ** for methods, binding captures are evaluated at **method entry ** (per call), since there is no “closure creation time” for methods.

4) Actors and isolation constraints (initial conservative rule)

To avoid suggesting actor isolation where self is not strongly held, the initial rule is:

  • For actor instance methods, any capture list other than [self] requires nonisolated.

Example:


final actor WorkerActor {

    nonisolated func runLoop[weak self]() async {
        // long-running / infinite loop; self: WorkerActor?
    }
}

If a developer wants actor-isolated behavior, they must keep [self] (the default) and manage lifetime explicitly, or hop to the actor explicitly when needed (outside scope for this pitch).

Examples

Long-running async task that should not keep the owner alive


final actor WorkerActor {

    private var loopTask: Task<Void, Never>?

    func start() {
        loopTask = Task { [weak self] in
            await self?.runLoop()
        }
    }

    nonisolated func runLoop[weak self]() async {
        while true {
            await self?.doWork() // fast work
            // do some work without requiring the actor to be kept alive by the method itself
            // observe cancellation
            try? await Task.sleep(nanoseconds: 1_000_000_000)
        }
     }

    deinit { loopTask?.cancel() }
}

Making implicit self explicit (documentation / readability)


final class Example {
    func doWork[self]() { /* same as today */ }
    func doWork[none]() { /* no implicit self */ }
}

Source compatibility

Purely additive. Existing code continues to compile unchanged.

Alternatives considered

  • Always use closure capture lists at call sites, e.g. Task { [weak self] in ... } (works but doesn’t help with *method-level * implicit self and discourages reusable APIs).

  • Move logic into static/free functions + pass wrappers (works but harms ergonomics and readability).

Prior art

  • Swift Forums discussions about Task { [weak self] ... } inside actors highlight why weak references don’t preserve actor isolation and document real confusion around long-running tasks and lifetime.

  • A Swift Forums thread proposed making capture semantics explicit for **local functions ** and discussed allowing capture lists for them, which is a close syntactic precedent for extending capture-list syntax beyond closures.

  • Related pitches about declaring capture semantics when passing method references show recurring developer pain around implicit self capture and lifetime.

Open questions

  1. Preferred syntax among A/B/C.

  2. Design choice: explicit-capture mode ([] / [none]) vs opt-out-only ([no self]).

4 Likes

This becomes a more limited form of a static method, where instead of referring to a metatype, self cannot be used at all.

The main difficulty with your feature is that a class method borrows a reference to self from the caller. This means that a method is allowed to copy this reference, which increments the reference count, and it is allowed to destroy those copies, which decrements the reference count, but it cannot destroy more copies than it has created, so the reference count must always remain at least 1. Converting a strong reference to a weak reference decrements the reference count, so you either must copy the reference first (which defeats the purpose of the feature), or you would have to restrict these "capture lists" to only appear in consuming methods (which are allowed on a class, but not the default).

One way to think of it is this. A capture list on a closure is only useful because a closure can outlive the dynamic extent of your method, so the method can return to its caller, which may then release the strong reference to self while the closure value is still alive.

3 Likes

I feel like this is missing the bigger picture, which is that a class should not manage Tasks which capture self.

(Obviously it can, if you're willing to take extreme care forever as the code gets maintained, but it's far simpler to make a blanket rule that it should not, and then rearchitect from there)

async + refcounting is basically a disaster zone; I also have a personal rule that there should be no public classes for very much this reason.

1 Like

Can you elaborate on your rule? I feel like I’m still struggling with the right patterns for this kind of thing.

Refcounting is great until you accidentally create a retain cycle. Although that’s an omnipresent threat, and no one rule is going to eliminate all issues, there are some things that mitigate it:

  • normal Swift design patterns don’t create complex object graphs; trees are more common
  • closures that capture self are common sources of accidental cycles, but the compiler warns about most cases (curried methods and captures within nested closures being the most egregious offenders that evade warnings)

That leaves a few cases where there are still easy accidents:

  • The Task constructor suppresses the compiler warnings for implicitly retaining self. It likely does so because weak and unowned cause even more subtle issues, but it does make it easy to make an accidental cycle.
  • async methods retain their receiver for their duration

These two are the focus of this thread; my answer here is "A class must not manage Tasks on behalf of itself" — Tasks always have to be managed "one level up" your object tree from whatever wants to be async. Basically what you get if you follow this advice to its logical conclusion is that your application framework should deliver events to async handlers, leaving all application code able to be async. Hummingbird does this well. SwiftUI does not.

With all the major causes of cycles dealt with, what's left? I said this at the beginning:

normal Swift design patterns don’t create complex object graphs; trees are more common

But that's what's left — accidents where an object gets retained for longer than expected. It could be because it's the receiver of an async method, it could be because it's a (non-self) capture of an async closure, it could be a simple cycle in an object graph, there are plenty of possible causes, but they all occur when a class is available for "other people" to store and reference and use.

Keeping classes as private implementation details of value types is a technique that can mitigate these problems too — it means that you can ensure the object can't be part of an arbitrary graph and therefore can't be involved in a cycle. Notice that the Swift stdlib sticks pretty closely to this principle; there are very few public classes (although there are plenty of internal and private ones); KeyPath is a notable exception, and ManagedBuffer (which exists exactly to be the class used as a private implementation detail of a value type).

3 Likes

This is a great set of insights, thanks. I’ve seen this “public value type/private reference type” pattern everywhere in the Swift standard library, but the purpose never quite clicked before this.

I’ve also been stumbling towards the “A class must not manage Tasks on behalf of itself" pattern with some recent code I’ve been writing to manage long-running background tasks, but it wasn’t immediately obvious to me.

Thanks for the reply.

Yes, that’s exactly how I see it. In practice, [] / [none] is mostly about making the developer’s intent explicit: they wrote a capture list, but intentionally didn’t capture anything, which enables explicit-capture mode and disables implicit self. This part isn’t fundamental to my pitch, so I’m open to different spellings (or even dropping [none]) if there’s consensus that it doesn’t add value.

I understand the issue and agree with your description of the current constraint: under today’s calling convention the method borrows self from the caller, and converting a strong reference to weak implies decrementing the refcount, which you can’t do unless you first create an extra strong copy (which largely defeats the purpose) or you restrict this to consuming methods.

However, my core point is precisely that it may be worth re-examining this ownership contract at the method boundary, at least for async methods, because it would address a real (and common) concurrency pain point. The “receiver is kept alive for the entire dynamic extent of the async method” rule is usually fine for sync code, but in async it can lead to unexpectedly strong lifetime dependencies for long-running work (loops, background tasks, etc.), even when you started from a weak capture at the call site.

One way I’d like to frame it is as two stages:

  1. Method entry / evaluating the method capture list - self is guaranteed by the caller (as today).

  2. The rest of execution (including across await) - only what’s explicitly listed in the capture list is guaranteed (e.g. weak self), rather than implicitly keeping the receiver alive until the method completes.

Yes, this implies an opt-in change to the ownership model for the receiver (or something equivalent in effect). I’m not attached to the exact mechanism - we might need a distinct opt-in contract - but I think the problem is important enough (especially for concurrency) to discuss that direction explicitly.

Today you can approximate similar behavior, but it’s awkward:

class Example {
    var likeFunction: () -> Void {
        { [weak self] in
            // ...
        }
    }
}

let example = Example()
example.likeFunction()

It works, but it’s not something I’d want in real code because it harms readability/ergonomics.

For sync methods, I also think this feature is useful for “method reference as closure” cases. If the developer knows a method will be passed as an escaping closure, it would be nice to express the capture semantics once, at the declaration site, rather than wrapping it every time.

Today you typically write:

class ExampleService {
    private var onAction: () -> Void = {}

    func setAction(_ onAction: @escaping () -> Void) {
        self.onAction = onAction
    }
}


class Example {
    let service = ExampleService()

    init() {
        service.setAction { [weak self] in
            self?.handleServiceAction()
        }
    }

    private func handleServiceAction() {
        // ...
    }
}

Whereas this feels cleaner to me:

class Example {
    let service = ExampleService()

    init() {
        service.setAction(handleServiceAction)
    }

    private func handleServiceAction[weak self]() {
        guard let self else { return }
        // ...
    }
}

We could go even further and, when passing a method reference as a closure (especially into an @escaping parameter), emit a warning that asks the user to explicitly specify a capture list, to avoid an implicit capture of self.

I’d love to hear your opinion on the pain point I’m describing here. Also, setting aside implementation concerns, do you think the proposed syntax and overall approach to solving the problem are a good fit?

Thanks again - your explanation of the borrow/refcount constraint is exactly the crux, and it helps clarify that the discussion is really about whether we want an opt-in change to the receiver ownership contract (at least for async) rather than “just syntax sugar.”

1 Like

Thanks for the reply.

I think this shifts the discussion away from the core of the pitch. A “blanket rule” like “a class should not manage Tasks that capture self” is an architectural guideline that can reduce risk, but the need for extra rules is also a symptom that there’s a real sharp edge in the current model (Task + async receiver lifetime + refcounting).

Swift is used by developers at very different experience levels, and even very experienced people make mistakes - especially as code evolves and gets maintained by others. One of the language’s jobs is to make the safe path the easiest and most obvious one, and to make the dangerous cases loud (via diagnostics/warnings), so you don’t have to rely on tribal rules that are easy to violate.

My point isn’t “ignore architecture”, it’s “provide a declarative, language-level way to express the intended capture/ownership semantics at the method boundary (especially for async)”. Then, even if a project does manage Tasks at that level for practical reasons, the code can be less fragile and more readable, without depending entirely on a rule like “never do this.”

2 Likes

I completely agree that what I’m saying shifts the discussion away from the pitch, and the reason I’m doing so is because I don’t think the pitch moves the language in a helpful direction.

A [weak self] capture in an async closure is completely undone by a guard let self, for example, which is also a natural thing to do.

I think there are four real problems that you’ve raised:

  • Task constructors implicitly capture self strongly
  • Task constructors allow [weak self] and [unowned self] captures
  • Async methods can be called on weak receivers
  • Unapplied method references implicitly capture self strongly

In each case, I think the correct solution is for the language to warn/error when a reference type is involved in one of these situations.

1 Like

There’s one more I’d add to the bucket, that’s not async-related but falls into the general category of “the compiler isn’t doing enough to help avoid cycles”:

acceptsClosure {
    acceptsClosure { [weak self] in
    }
}

In this case, where the only use of self in the outer closure is as a capture list in the inner closure, the compiler doesn’t warn that the outer closure should explicitly declare its capture semantics

1 Like

I understand that you’re intentionally stepping back to the “bigger picture”, but I still think the pitch itself can be practically useful, for the reasons I outlined above: I want a declarative tool at the method declaration site that lets authors express the intended lifetime/capture semantics, instead of relying only on discipline.

You’re absolutely right about that - but I’d argue this isn’t really the core point of what I’m proposing. If anything, it illustrates how easily “safe intent” can get blurred by very natural patterns. At the same time, guard let self is an explicit choice by the developer to make the lifetime strong again, and that’s fine when that’s what they want. My point is to provide a way to avoid implicit strong retention (especially across await) when that’s not what was intended.

On your list of four problems and the idea to “warn/error in each case”: I generally agree that diagnostics would help a lot here (and I’ve already suggested a warning for unapplied method references).

But I’m not convinced warnings alone are sufficient: they tell you “this is dangerous”, but they don’t provide an ergonomic, expressive mechanism that also improves readability and reduces maintenance risk.

So, setting aside “this is an architectural problem”, do you have any concrete objections to my approach (including the syntax / model) - i.e. reasons why you think it wouldn’t move the language in a helpful direction?

I like putting it inside {} braces on the grounds that the syntax is similar to that of a closure. Unless closure list has to be part of function signature. Should it? If should - then one of the other two.

Could I use that in a protocol? What would be the rules IRT implementing a protocol method with mismatching capture list? (weak v non weak, vice versa, etc). Ditto question for subclassing with a misstating list.


As for implicit captures of self in Task initialisers (@_implicitSelfCapture) – I think this is a foot gun.

1 Like

Of the problems you've raised, your "method capture lists" solves maybe the "async function calls retain their receiver" one.

  • it doesn't address Task constructors allowing implicit capture of self
  • it doesn't address unapplied method references implicitly capturing self
  • it doesn't address Task constructors allowing [weak self] and [unowned self] captures — in fact, it kind of actively leans into this misfeature

If you actually use this in practice on an async function, async func f[unowned self](...) { ... } is never necessary and potentially just causes crashes — to use it you must know that someone else is retaining self for at least the lifetime of the call, and therefore using a strong self can never change the behavior.

If you instead use async func f[weak self](...){ ... } then the first person who gets annoyed by all the self?.abc in body will do guard let self else { return } (as is common practice in synchronous code), and that will immediately return us to the "strong self" world, negating all benefit of the syntax. You'd have to introduce warnings or similar for guard let self in async contexts to make any practical benefit here. And maybe we should do that anyway, but there are practical times where you can use that safely in async code too, so I suspect it wouldn't be uncontroversial.

As someone else pointed out, func f[](...){...} is exactly equivalent to a static method.

I can't imagine the feature is ever useful in synchronous code.

And even if you add these features, you still need a warning to direct people to use them — something like "async method called on weak receiver; the receiver will be retained for the duration of the call". And once you've got that warning, I'm unconvinced you need (or even want) async func f[weak self](...){ ... } as a solution; the chances are that you need to rearchitect more than just this one method call to get the memory management right — something like moving the task creation up a level in your object hierarchy such that the asynchronous closure doesn't have to capture self in the first place.

I propose instead,

  • Task constructors other than Task<Void, Never> should not return @discardableResult (I think this is already in progress)
  • Task constructors should warn if you capture self of a reference type; something like "warning: capture of self in asynchronous closure may cause the lifetime of self to be extended indefinitely". Require an explicit [self] capture list to silence the warning.
  • Task constructors should disallow capture of [unowned self]; it's guaranteed to be at lest unnecessary. Something like "error: capture of unowned self in asynchronous closure is never required".
  • Task constructors should warn if you capture [weak self]. This one's harder to know how to silence the warning, or how to word it. Needs more thought.
  • [weak self] async closures should warn about guard let self that spans any await; something like "warning: self retained across await may cause the lifetime of self to be extended indefinitely"; this could be silenced with parens around (self) I think?
  • calls to async functions on weak receivers should warn; something like "warning: call to async function f may extend the lifetime of x indefinitely"; this could be silenced with parens around the receiver
  • the compiler should warn when creating an unapplied method reference to a reference type self, as it does when capturing self in a closure. This could be silenced with parens, and offer a fixit to create a closure with an explicit capture list
  • the existing warning when capturing self implicitly should be generated even when the only reference to self is the capture list of an inner closure (just a bugfix to the existing diagnostic)

I agree that none of this helps architecturally, but at least you get the compiler telling you when you're on the wrong track.

As for how the compiler could provide feedback about better architecture, other than deprecating Task constructors entirely, I'm definitely keen to hear some ideas!

Sure, but this could be addressed separately (IOW this is a different pitch).

What are those? Please show an example of the issue.

Please clarify your point... Task initialisers allow those weak/unowned, do they not?

That's what unowned is... same for variables, use with extra caution. Some teams even disallow it via linters.

I don't think the conclusion here is entirely correct... Sure thing about quard let self else { return } need, but the difference is that self is not strongly captured so it will be possible to get to the return (or whatever exit strategy is) of that guard statement. This is the crucial difference, and this is what would make functions more similar to closures in regards to captures.

Which is not bad per se... we do have many places in Swift where we could do the same thing by various means.

Speaking of which, I was thinking of this sugar for [weak self] passing:

    weak func foo() { }

Why not. Would work similar to passing a closure to a timer, or, say:

// despite the "async" here the functions / closures
// passed to Dispatch are synchronous

Dispatch.main.async { [weak self] in ... }

// same:
let weakClosure = { [weak self] in ... }
Dispatch.main.async(execute: weakClosure)

// same with this pitch:
func weakFunction() { [weak in] ... } // or whatever the syntax is
Dispatch.main.async(execute: weakFunction)

Good ideas in their own right. Not necessarily "instead" though, one does not preclude or supersede another.

it doesn't address unapplied method references

What are those? Please show an example of the issue

class C {
    func foo() {}

    func bar() {
        let closure = self.foo // unapplied method reference, equivalent to
        let closure = { [self] in self.foo() } // except this one warns you about the retain cycle
    }
}

That's what unowned is... same for variables, use with extra caution. Some teams even disallow it via linters.

In synchronous code, "outlives" is something it's (sometimes) easy to reason about. That said, at least half the time people use unowned, it's wrong. If we could go back and make everyone use weak with !, I'd probably do it.

In asynchronous code, it's basically only reasonable to reason about it if you're explicitly awaiting the thing that has the unowned reference, in which case you didn't need the unowned reference.

I don't think the conclusion here is entirely correct... Sure thing about quard let self else { return } need, but the difference is that self is not strongly captured so it will be possible to get to the return (or whatever exit strategy is) of that guard statement. This is the crucial difference, and this is what would make functions more similar to closures in regards to captures.

I don't really understand this point. After guard let self else { return }, self is strongly retained for the rest of the function, more or less as if it had been strongly captured in the first place. If there's any await after guard let self else { return }, it's almost certainly not doing what you intended.


I'm not trying to say that you couldn't do what the OP is proposing, it just seems like an edge case on edge cases, that doesn't really address any of the actual pain points that motivated the post in the first place.

If I have this:

func process(_ whatever: Whatever) async { ... }

func longRunningMethod() async {
    for await whatever in something {
        await process(whatever)
    }
}

/* later */ {
    self.task = Task {
        await self.longRunningMethod()
    }
}

and I realize I've made a cycle, if I now change it to

func longRunningMethod[weak self]() async {
    guard let self else { return }
    for await whatever in something {
        await process(whatever)
    }
}

Then I still have the exact same problem I always had.

If instead, I write:

func longRunningMethod[weak self]() async {
    guard let something = self?.something else { return }
    for await whatever in something {
        await self?.process(whatever)
    }
}

Then I still have a bug where the task spins indefinitely when self vanishes.

If instead I write

func longRunningMethod[weak self]() async {
    guard let something = self?.something else { return }
    for await whatever in something {
        guard let self else { return }
        await process(whatever)
    }
}

Then I still have most of the original bug, because self is strongly retained across process.

If I propagate [weak self] to process, and write:

func process[weak self](_ whatever: Whatever) async { ... }

func longRunningMethod[weak self]() async {
    guard let something = self?.something else { return }
    for await whatever in something {
        guard let self else { return }
        await process(whatever)
    }
}

Then a) we still haven't seen how hard this change was to make in the body of process, and b) I still have the same bug as the last iteration because my guard let self retains self across the call to process. I guess that'd have to be a warning ([weak self] method called on strongly-retained receiver).

If instead I write

func process[weak self](_ whatever: Whatever) async { ... }

func longRunningMethod[weak self]() async {
    guard let something = self?.something else { return }
    for await whatever in something {
        if self == nil { return }
        await self?.process(whatever)
    }
}

Then arguably we are now more or less correct, though we can still consume one element from something that never makes it to process, but that's probably true in most of the iterations.

And I don't know about you, but I don't find this readable, I don't find it maintainable, and I don't think that the leak-free version would survive the next person's effort to maintain this code (or possibly even my next effort to maintain the code). Leak-free-ness has been acquired at the cost only of immense vigilance, which is already possible in the language today.

1 Like

I think that's worth comparing and contrasting with "the compiler yells at you when Task captures self"; if instead I'd gone from having

class C {
    func process(_ whatever: Whatever) async { ... }
    
    func longRunningMethod() async {
        for await whatever in something {
            await process(whatever)
        }
    }
    
    func startAsyncWork() {
        self.task = Task {
            await self.longRunningMethod()
        }
    }
}

class D {
    let c = C()

    func startAsyncWork() {
        c.startAsyncWork()
    }
}

to

class C {
    func process(_ whatever: Whatever) async { ... }
    
    func longRunningMethod() async {
        for await whatever in something {
            await process(whatever)
        }
    }
}

class D {
    let c = C()
    var task: Task<Void, Never>?

    deinit {
        task?.cancel()
    }

    func startAsyncWork() {
        self.task = Task {
            await c.longRunningMethod()
        }
    }
}

Then my problems would be solved; C is now very simple, D is still pretty simple, and I've gone from trying to use weak self vanishing as a proxy for task cancellation, to actually using task cancellation in the way it was intended. No vigilance is required, aside from a new compiler warning to hint that I'm headed off the rails.

I also think that it's worth pointing out that this pattern/antipattern basically occurs in code for Apple platforms because Apple's SDKs are insufficiently async-aware. People want async work in response to notifications, to button handlers, to app launch, to app termination requests, etc., and the SDKs don't provide that functionality. You don't see people trying to do this in CLIs, because you can use structured concurrency directly from main or ArgumentParser. You don't see people trying to do this in Hummingbird, because request handlers are already async. It's effectively an ecosystem problem leading people who shouldn't need to write Task constructors, being forced to.

So whilst I say "the solution is to move the task management up one level", maybe I'm thinking too small — maybe the solution is to write a large SDK extension that provides the missing functionality at the highest level, and offers UIKitAsync.ApplicationDelegate, FoundationAsync.NotificationCenter, SwiftUIAsync.Button (not an easy problem), etc.

Yeah, I thought you mean that. So with "weak" methods this could change.

BTW, there's no warning (and no retain cycle) in that particular fragment.

The crucial part is happening "Before"... e.g. could the object go away?

Illustrating example:

import Foundation

class C {
    deinit {
        print("deinit")
    }
    func function() {
        print("non weak Function \(String(describing: self))")
    }
    lazy var closure: () -> Void = { [weak self] in
        print("weak closure \(String(describing: self))")
    }
    func testWithFunction() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: function)
    }
    func testWithClosure() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: closure)
    }
}

var c: C? = C()
print("start")
c!.testWithClosure()
//c!.testWithFunction()
print("zeroing")
c = nil
RunLoop.current.run(until: .distantFuture)

BTW, this example also illustrates that this feature is not just about async and Task (regardless of whether those could be improved), it's more fundamental.

Thanks for the example - but I think it’s a bit misleading as a comparison, because it makes your approach look more compact/clear than it really is.

In the first version, D isn’t actually needed: C is self-contained, it just has an obvious potential cycle. In the second version, D is required to satisfy the pattern (“a class shouldn’t manage Tasks that capture self”) - which means the comparison is no longer apples-to-apples (I’m not saying the pattern is bad, just that it’s a different tradeoff).

Also, maybe this is just a simplification in the snippet, but the “pattern” version still has a potential leak if longRunningMethod can be unbounded: the Task holds a strong reference to self in D, so deinit can’t run, and both D and C stay retained.

Thank you so much for the great questions and the support :slightly_smiling_face:

About Option C: originally, when I was designing this, I thought Option C would be the most familiar. But if you show all the “hidden” parts of a closure, it looks like this:

let closure = { [weak self] (value: Int) -> Int in
    ...
}

I basically took that semantics and applied it to methods:

func function[weak self](value: Int) -> Int {
    ...
}

That’s why I’m currently leaning most strongly toward Option A - it feels the most consistent with how developers already think about capture lists.

On protocols / inheritance: in the first version of the design I thought this mostly didn’t matter, because the capture list doesn’t change the call semantics (it’s about how self and implicit lookup behave inside the body). So I originally imagined cases like this being fine:

  • a protocol requires func doSomething()

  • one implementation uses func doSomething[weak self]()

  • another uses plain func doSomething()

  • and all of them are called the same way via ExampleProtocol

But after thinking more about the “warn when turning a method reference into a closure” idea (and implicit self captures in general), I’m now leaning toward needing a more explicit contract here.

One possible direction (not a fully formed proposal yet - more of a thought): add an annotation like @closurable that requires an explicit capture list whenever a method is intended to be safely convertible into a closure.

protocol ExampleProtocol {
    @closurable
    func doSomething()
}

final class Example: ExampleProtocol {
    func doSomething() { ... }          // error: method must be closurable — specify a capture list explicitly
    func doSomething[self]() { ... }    // ok
}

For overrides, I’d see the rule like this:

  • if a base method is @closurable (or already has a capture list), then the override must also follow those rules (i.e. must specify a capture list explicitly);

  • changing the specific capture mode ([self] ↔ [weak self]) is not forbidden;

  • but you can’t “weaken” the contract the other way: if the base method wasn’t @closurable, then the subclass may add it, but not vice versa.

class Base {
    func doSomething[self]() { ... }
}

class Sub: Base {
    override func doSomething() { ... }            // error: must specify a capture list explicitly
    override func doSomething[weak self]() { ... } // ok
}

And yes: I’d make @closurable implicit whenever a capture list is present.

Everything above is still not a complete pitch - just a direction I’m thinking about. If someone has a better set of rules for protocols/overrides here, I’d really appreciate ideas.

1 Like

my bad, It should've been Task { [c] in to avoid the cycle. Again, the lack of warning on self-capture bites.