SE-0478: Default actor isolation typealias

Folks,

The review of SE-0478: Default actor isolation typealias begins now and runs through May 5, 2025.

This proposal is tied to the Improving the approachability of data-race safety vision.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager via the forum messaging feature. When contacting the review manager directly, please keep the proposal link at the top of the message.

Trying it out

If you'd like to try this proposal out, you can download a recent main development snapshot and enable the DefaultIsolationTypealias experimental feature flag.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at swift-evolution/process.md at main ¡ swiftlang/swift-evolution ¡ GitHub.

Thank you,

Steve Canon
Review Manager

6 Likes

-1 for the form, +1 for the function.

Having a specially-named specially-visible typealias sounds too magical and practically undiscoverable.

I'd personally prefer an additional syntax to assign attributes to entire files, similar to what kotlin has.

12 Likes

I agree that this is desirable functionality, but I'm also weirded out by the form, particularly typealias nonisolated = Never.

There's a pitch out there for (certain) compiler flags to be set on a per-file basis (default actor isolation, concurrency mode for swift 5, strict memory safety, warning levels, etc.). I don't see the need for this one thing to be different to the rest — I'd prefer to advance that more general proposal instead.

6 Likes

Spelling this as a typealias is a confusing and not generalizable. The general solution doesn’t require lots of thought and exploration before implementing. The pattern is well established by other languages (e.g. #pragma in C/C++, {-# pragmas #-} in Haskell) and equally applicable to Swift.

3 Likes

We should take into account though that the typealias approach is the most consistent with present day Swift – given how default literal types can be influenced, the distributed actor system default can be set, and we're also looking to offer the same typealias way of setting the default global executors (DefaultExecutorFactory) -- though that one is pending review still (pitch here).

So the "typealias or not" part of the discussion is somewhat bigger than this specific proposal, it also has impact on the way global executors would be configured (in fact, this proposal already had impact on those, as they moved to the typealias approach).

I think this proposal is good, and the use of private / fileprivate to control how wide the impact the typealias has is quite nice.

I'm somewhat unsure about the unusual spelling of the nonisolated type(alias). We're doing this trick in order to make it look like we're using the keyword in type position, but it's not the keyword and therefore not a special rule etc. It does mean we can write it in type position though, including:

... where A == nonisolated 
// meaningless, unlikely to compile and give false impression
// of "working" in realistic code examples that actually use `A`
func hm() -> nonisolated { ... } // just silly

or other similarly nonsensical signatures. These are a bit silly, and arguably harmless, and perhaps In practice probably most if not all such nonsensical spellings would fail to compile, so at least we're safe from compiling code which looks like it may "conditionalize on isolation".

Will we regret this if at some point we'd have some way of having meaningfully "only if nonisolated" expressiveness in the type system? I'm not sure we'll have such, but it is giving me a pause if we're just using Never to express this.

--

Exploring ideas if we can get rid of the nil also meaning nonisolated in other contests and use nonisolated instead as a value.

A stated goal of choosing nonisolated as the spelling is to reduce the number of ways we can spell "nonisolated", and the remaining elephant in the room of the ways to spell it is: isolated (any Actor)? = nil, or nil specifically.

Since we know conditionalizing code on where A == nonisolated / Nonisolated isn't realistically a thing... I am wondering if another option to think about here is to make use of the nonisolated as one would expect -- as a value, and extend the custom string representation of (any Actor)? to handle the nil appropriately?

//struct Nonisolated {
//    fileprivate init() {}
//}
typealias Nonisolated = (any Actor)?
// typealias nonisolated = Nonisolated

let nonisolated: Nonisolated = nil
    

extension Optional: CustomStringConvertible where Wrapped == any Actor {
    public var description: String {
        switch self {
        case let .some(act): "\(act)"
        default: "nonisolated"
        }
    }
}

Which does have the problem of A == Nonisolated now meaning (any Actor)? but perhaps we could warn about "actually using" that type in locations other than the DefaultIsolation.

The isolated parameters use-case though flows nicely with this then. It is true however that this is rarely spelled out explicitly, so tbd how much we care about it:

func call(isolation: isolated (any Actor)? = #isolation) {
    print("#isolation = \(String(describing: isolation))")
}

await call()                       // #isolation = Swift.MainActor
await call(isolation: nonisolated) // #isolation = nonisolated

In this world though we'd have to spell the typealias using the uppercase:

typealias DefaultIsolation = Nonisolated

which isn't the same as the keyword, but looks somewhat expected, and it is the same actual "word" just uppercased because it is in type position.

It does probably mean that we'd want to reserve this type's use to only be in the specific DefaultIsolation... so, in this world, would we warn or error about the following?

func call(isolation: isolated Nonisolated = #isolation) { 
// error: don't actually use this type in signatures?

Sadly we can't eat the cake and have the cake, i.e. typealias nonisolated = Nonisolated AND let nonisolated: nonisolated = ....

Either way, I am wondering if we can do something about the nil "nonisolated" while we're trying to solve this default isolation type. If not, then not, and we'll keep sticking around with that nil -- maybe we can do that custom string description independently of this idea though, might be nice in itself?

edit: The more I looked at this written up the more I had the same concern, that we're pretending a value looking like a keyword for other purposes. So this would have the same problems really. So it's back to the original question if we're happy to do trickery with reusing the same exact word for other purposes, or not.


Either way, I'm in support of the proposal's typealias approach and would just want to make sure we're really confident about that nonisolated won't cause us trouble and confusion down the line. Maybe along the way we can do something about the nil spelling as well.

4 Likes

Sorry, re-posting here since the proposal is in active review rather than pitch:

A thought—

Today, it is possible to test IntegerLiteralType.self == Int.self (for example); not sure if it's the best solution for anything, but it is possible to do and works as expected.

I would imagine that someone, somewhere, will want to write DefaultActorIsolationType.self == MainActor.self. Can it work as expected (even if it's not the best solution for anything)?


A tangentially related thought—

Should we infer the typealias to be MainActor for top-level and/or playground code so that our documentation examples work again without caveats that upend progressive disclosure?

5 Likes

Using type aliases for per-file compiler settings is a design smell that’s too strong for my taste. -1

TBH, I am disappointed that this approach is being considered for other active pitches. I fell in love with Swift because of its API design philosophy, and this approach feels antithetical to that.

6 Likes

This bit about being generalizable is stated as an absolute but I think it strongly depends on the lens through which someone views this feature. Conversely, one could state that this is already applying the generalization of a feature that has existed in the compiler for many years:

  • The default types of inferred literals can be changed by setting typealiases like typealias IntegerLiteralType = UInt8 in a file. Likewise for strings, floating point values, and a few others.
  • The distributed runtime allows the default actor system to be changed by declaring typealias DefaultDistributedActorSystem = Something.

Those features may be less commonly used, but they do exist.

The only part that gave me pause was the introduction of a no-op type(alias) like nonisolated. But I've come to peace with that from the point of view of how do you explain to someone who is reading that spelling what it means. "typealias DefaultIsolation = nonisolated means that the default isolation for decls in this file is nonisolated. You can also set it to MainActor if you want." Clean, simple.

I'm not sure if there would ever be a future direction to allow custom actors to be used here, but that would naturally follow from this syntax in a way that's more awkward to express using the hypothetical settings syntax.

Given the precedents above, I disagree with the premise that we need to invent an entirely new file-specific settings syntax in order to add this feature. Especially when, based on the earlier discussion of that feature, there simply weren't that many settings that we wanted to be able to apply on a per-file basis, and likely there wouldn't be in the future. I think there were 3 possible settings being proposed, including this one?

To folks who typealias isn't "heavy" enough and blends in, I can see your point of view, but I'd counter that I don't think it's necessarily an improvement to have a "noisy" grab bag of potentially unrelated settings somewhere in the file either to scan through.

I don't think having a special syntax for settings would make this stand out more, because how often are you only staring at the top of your file to see that information? If you're navigating somewhere else inside a file that's a few hundred or thousand lines long, I don't think that brief #swiftSettings([...]) stanza is any more discoverable than a brief typealias in the same location. There are other ways that we can take advantage of tooling here, for example by making swift-format always put the DefaultIsolation typealias at the top of the file just under the imports (or above the imports, even?). I'm not convinced that if I jump to the top of my file, I'll have a much easier time finding this critical information if it's spelled something like #swiftSettings([...]) instead of typealias DefaultIsolation = ... just because it has a # in front of it? Has more tokens in general?

+1 for the currently proposed syntax. It nicely adopts existing precedent in the language rather than having to be front-loaded by an entirely new heavyweight syntax that tries to group together a disparate set of things into a single concept.

3 Likes

I think there should at least be an attribute so the user knows if the DefaultIsolation was skipped because it doesn’t meet the right conditions. Maybe something like the following?

@defaultIsolation
typealias DefaultIsolation = MainActor

I also support the idea of using an expression macro. However, having no explicit syntax (either an attribute/expession macro) for this feature and just skipping the typealias would be too error-prone.

According to the Detailed Design, the compiler already diagnoses incorrect usage. What would the attribute do that isn't already being proposed here?

The proposed ability to toggle “warnings as errors” in a specific file does not fit into this mold.

However, these examples do fit into a more general pragma-like feature (which is spelled #compilerSettings in the linked pitch). One can imagine #compilerSettings(defaultDistributedActorSystem: "Something") being a supported syntax that can be copy-pasted between a source file and a SwiftPM manifest.

1 Like

And that's totally fine! I never argued that every future feature should have to use a typealias syntax. In fact, I was saying precisely the opposite when I referred to #swiftSettings/#compilerSettings as a "grab bag" of various settings above.

The only reason to do this would seem to be that some folks don't like the established precedent of using typealiases in the language to control some of these type resolution mechanics. Why would that be a strong enough reason to introduce a new and different syntax for an existing feature?

IMO the reason is very strong, concurrency is pervasive across all types of code and you must always keep the default in mind while reading code. Frankly, that distributed actor systems uses a typealias isn't comparable in scope to this requirement (and likely most posters in this thread would believe they made the wrong choice using a typealias).

-1 on this.

I vote a strong -1 on this, because typealias feels very casual for something very important.

Better to have a compiler directive. For example: something like #pragma (isolation, default :...).

2 Likes

-1 for me.

The concept itself is sound and definitely worth consideration, however, using typealias declarations for achieving this feels like a bad approach for achieving this goal; it's a bit too "magical" and if adopted would likely lead to more of these odd syntactical constructs for comparable features down the line

Given the nature feature being proposed, I think that a much more appropriate syntax would be something like suggested above:

@defaultIsolation(MainActor/nonisolated)

// or

#pragma(isolation, default: MainActor/nonisolated)

// or

#compilerSettings(defaultIsolation: MainActor/nonisolated)

// or

#swiftSettings(defaultIsolation: MainActor/nonisolated)

All of these syntaxes are prescriptive easy enough for any reader to reason about. Furthermore the latter three syntaxes also allow for simple addition of other per-file compiler settings without introduction of additional syntax.

4 Likes

I really like the typealias direction. There’s strong precedent for this in the language, especially with things like default literal types (as @ktoso pointed out) and constraining protocol associated types. So if anything, it’s the alternatives that feel “magical” to me. Using standard Swift access control to define the scope of the default is also a nice touch. That's intuitive and already familiar to most Swift developers.

That said, I also share @ktoso’s hesitation around the nonisolated typealias. Given Swift’s recent foray into value generics, I wonder if something like this wouldn't be entirely out of place:

typealias DefaultIsolation = nil

This would mirror SE-0466 nicely.

2 Likes

I don't really mind the typealias approach, but this:

public typealias nonisolated = Never

is as confusing as it can get: a wrong-cased type masquerading as a keyword, available globally in any context.

I find directly using Never would be actually less confusing:

private typealias DefaultIsolation = Never

On that note, maybe it's just me but I'd prefer Void here instead of Never. Never tends to make things it attaches to uninstanciable and uncallable, while Void just represents an empty value.


Now, the only weird thing that remains is that you can't write:

@DefaultIsolation
class Thing {}

… or to be more precise: you can do that but only when you set default isolation to the main actor. Not that it's very useful to be able to write that, but it'd be easier to explain what it does in a less magical way.

5 Likes

I'm staunchly opposed to the proposed solution to the problem. It feels like misuse of type aliases, and we should instead consider other mechanisms for setting per-file compiler options.

4 Likes

Using a typealias should be avoided to be better prepared for what future may throw at us.

For example:

Old - [1]
#compilerSettings (concurrency, <parameter>: <value>)
#compilerSettings (concurrency, defaultIsolation: MainActor)
#compilerSettings (concurrency, ...)
#language (concurrency, <parameter>: <value>)
#language (concurrency, defaultIsolation: MainActor)
#language (concurrency, ...)

[1] Thank you, @scanon, for pointing out the difference between compiler settings and language/library settings.

Somewhat tangential to what Holly has proposed, but relevant to some of the suggestions: it is (IMO) a mistake to think of this as a "compiler setting". This is a typealias that changes the meaning of a builtin-type, and hence the semantics of other expressions. That's a language (or library) setting, not a compiler setting (the compiler's behavior is unchanged).

14 Likes