Easily capture the current global actor using async let?

I just ran into the following issue and was wondering if there was a nicer workaround that would automatically capture the current global actor when using async let.

Task { @MainActor in
  let a = await foo(mainThreadOnlyObject.property) // this is ok
}

Task { @MainActor in
  async let a = foo(mainThreadOnlyObject.property) // This is not safe. Not the main thread anymore.
}

Task { @MainActor in
  let property = mainThreadOnlyObject.property // if it's a value type
  async let a = foo(property) // ok again
}

In my case foo(...) was an objective c method with a completion handler. mainThreadOnlyObject was an objc realm object so I was able to use #3.

I was surprised to run into this issue. It seems like sort of a footgun given I already specified the main actor at the parent context.

That's because async let runs as a concurrent child task, on the global concurrent executor like all child tasks.

It was a design decision that I disagreed with when this was all being pitched, because it made more sense to me that async let should run on the same executor as the declaration that spawned it.

Incidentally, I don't believe there's currently a mechanism to run a child task on anything other than the global concurrent executor, but that may change with future enhancements to executor customization. (Whether there'll be a way to apply this to async let isn't clear yet.)

The upside of a concurrent async let is that it runs concurrently, rather than merely asynchronously, so it's clearly a way to make progress on 2 or more paths of execution at the same time. If not concurrent, it would at best be interleaved on the main thread, which I guess was seen as negating the concept.

The downside, of course, is that when you're swimming around in @MainActor code, the likelihood is that you often want asynchronous-but-not-concurrent paths of execution. For now, using nested Task { … } is the best alternative, perhaps.