Pitch #2: Protocol-based Actor Isolation

“Moving” Objects Between Actors

is it about move-only value types(moveonly struct, etc.)?
Will it need to implement move-only value types?

It seems like the reference semantics in a value type composed of value-semantic types comes from static variables and side-effects in computed properties/functions. But then, one can argue that there isn't much value semantics in Swift. And the truly value-semantic types can be easily extended with static variables, computed properties, or functions to make them reference-semantic.

I think ValueSemantic should be implicitly synthesized, because it's not really an opt-in feature like Hashable and Codable.

I think there are 2 pieces of information communicated in a protocol conformance: capability and intention.

When the feature enabled by a protocol is opt-in, these 2 pieces of information are bound together. For example, many types are capable of being hashable and codable, but unless they declare their intention of enabling the capabilities by conforming to Hashable or Codable, they can't be hashed or encoded/decoded. Conformance to Hashable and Codable is like labeling a box with a mailing address, where the label communicates both that the box is capable of being sent by mail, and that it's intended/allowed to be sent by mail.

When the feature enabled by a protocol is not opt-in, then the intention becomes pointless. If a type has value semantics, then regardless of its conformance to ValueSemantic, it exhibits value semantics when its instances are passed and copied. An explicit conformance to ValueSemantic is like labeling a box with the word "box", where the label only confirms that the box is indeed a box.

struct Foo {
    let bar: Int
}

Foo is codable, but without conforming to Codable, you can't use Foo(42).encode(to: /*...*/).

Foo has value semantic, and it has value semantic with or without conformance to ValueSemantic.

1 Like

Why does it matter where it comes from? It's called ValueSemantic, not ValueImplementation :)

Yep! That's why I think reference/value split is more about how you use a thing, not the type of it.

It is related but different - I renamed that section to "Transferring" Objects Between Actors to avoid confusion, thanks!

This is a really great framing!

I see ValueSemantic as naturally an opt-in feature. As pointed out up-thread, there are types which are compositions of value semantic types that are not themselves value semantic. This implies it should be opt-in. Furthermore, something that "happens to be a composition of value semantic types" today may not be tomorrow (in a resilient API evolution sense), so "implicit" synthesis is problematic.

The proposal tried to tip toe around the second issue by making implicit synth only happen for non-public types, but that just makes the language model more complicated. I would expect to get questions about "why did my code break when I marked my type public" on stack overflow for example.

I think this is the crux of the issue - as pointed out up-thread, your Foo example may not actually be value semantic, it depends on the implementation and intention of that int. It might be a database handle after all.

-Chris

3 Likes

I think this problem applies to other protocols to some degree too. For example,

struct DeepThought: Codable {
  let answer: 42
}

DeepThought is codable, with synthesised requirements. If a non-Codable instance property is added, it becomes un-Codable:

struct DeepThought: Codable {
  let answer: 42
  let question: Never
}

I guess the benefit of explicit conformance here is that the problem can be caught early, instead of waiting for type-checking or static analysis at the use site (e.g. passing an instance into an async function parameter in the case of ValueSemantic).

As discussed up-thread, ValueSemantic is more sensitive to static members and side effects, so I agree that requiring explicit conformance makes sense.

However, thinking from an API user's instead of author's perspective, explicit ValueSemantic conformance conceptually limits a lot of what a user can do without enforcing those limitations:

Borrowing @cukr's example:

struct TeamRef: CustomStringConvertible {
  static let numberOfTeams = 6
  // Can't allow [Int]-typed static variables here, 
  // because they enable reference semantics? 
  private static var kills: [Int] = .init(/*...*/)
  private static var deaths: [Int] = .init(/*...*/)

  /*...*/

  var description: String {
      "kd: \(kills)/\(deaths)"
  }
}

I assume that all standard library types will come with ValueSemantic conformance, and some of them such as Int and String have to be explicit because they're built on reference types. Then, can users still use them to enable reference semantics for other types, as the example above? If users are discouraged from using them for anything reference-semantic, there isn't any way to actually enforce the discouragement or nudge them towards a safe default.

Also, if the user wants to extend a ValueSemantic-conforming type they don't own with something that enables reference semantics for the type, can they still do it? For example:

extension String {
  static var somethingImportant: NSMutableString // could just be String, actually
}

I think the biggest problem is that we don't really have a solid definition of value semantics. When I think of value semantics, I often think in terms of if an instance and its properties (and their properties...) are passed or copied by value. Maybe I should think of this concept as a superset of value semantics.

Sorry for the long delay here... this proposal is definitely moving in the right direction. Here are a couple of comments:

  • Something like @UnsafeTransfer feels like it needs to be part of the core proposal, because the lack of an opt-out here either hampers adoption or pushes people to incorrectly introduce conformances to ActorSendable. However, the property wrapper implementation isn't actually that great, because it has ABI impact. I suggest that @unsafeTransfer be a real attribute and a core part of the proposal.

  • From a performance perspective, we should consider formalizing the notion of a "marker" protocol for ActorSendable / ValueSemantics, banning any of the dynamic features that would force us to emit a witness table in the metadata. For example, we could ban the use of such protocols in existential types and dynamic casts (no var a: ActorSendable or x as? ActorSendable in the source language) so that there is no ABI impact to the introduction and use of such protocols. This also means we don't have to worry about multiple conformances quite so much, which might help with rollout as (e.g.) lots of people add ActorSendable conformances to third-party types that will cause conflicts.

  • I don't think this proposal should be tied to value semantics at all. Yes, value semantics provide actor-sendability, but there's a natural layering here and you don't need to block progress on this proposal on value semantics. (Yes, I'm worried that discussion won't converge, given that it's been simmering for years)

  • I think the name ActorSendable isn't quite right, because (1) this could/should be more general than actors, if we go and apply this to captured local variables, and (2) it's not just about "sending" values, both due to captured local variables and also because we're currently allowing let values to be accessed from other contexts. How about Shareable, because it's safe to share values of the type across tasks/threads/actors? That could be paired with @unsafeShareable to disable the checking on a per-parameter basis.

    Doug

8 Likes

No problem, thanks for the feedback!

Just to clarify, why is ABI impact a problem? This is part of the signature for the method, so it seems perfectly reasonable to have ABI impact. There are no retroactive adoption issues that I'm aware of. If there is an ObjC bridging related issue it seems like it could be handled in the interface layer?

Interesting, that seems totally reasonable to me. This is a bit beyond my depth though, what does making it a marker protocol mean? Does that mean extensions on it would be prohibited or something else?

The major reason to tie them together is that value semantics is the vastly most common case and is independently useful - asking people to annotate their types multiple times seems really unfortunate.

I think that Sharable is good, happy to make that change. However, I think that this is directly tied to actors, even for captured variables, because the limitations are only applied to variables captured by closures that are passed across closure boundaries. Similarly, cross-actor properties on actors have the same check, etc. All the enforcement and special behavior happens at the actor boundaries, so I think that including the work Actor is appropriate. This also reduces the problem with Sharable being such a generic name.

I would recommend ActorSharable. Does that make sense to you?

I have a bigger question for you though: how best would you like me to engage in this process? I am happy to revise and push forward the whitepaper, but I don't have the time to actually implement this, so I don't think it will ever actually get to a proposal phase. Is someone willing to be a coauthor, or is someone willing to adopt the content and become the first author and help drive this?

-Chris

1 Like

I don’t have a strong opinion, but want to point out that the point of value semantics is precisely that no sharing actually happens. So this seems like a misnomer when applied to value semantic types. Sharing only actually happens for types that have internally synchronized shared state.

1 Like

This is a good point. I went through many verbs in the writeup and settled on the "Transfer" verb, which seemed to convey passing data between actors without implying "sharing" or "moving" and seemed more concrete than "sending". I'd argue for ActorTransferrable or something like that. It's a mouthful, but most people will work with ValueSemantic so it seems ok.

-Chris

I'm going under the assumption that we'll have to over-annotate as @UnsafeTransfer a number of parameters of types that, later, will themselves become actor-sendable through improved language features (actor-local types and such), and having @UnsafeTransfer be an ABI no-op and source-compatible to drop it once the type is actor-sendable would help those annotations go away.

Extensions are fine. I think it means you cannot have any requirements in the protocol, can't create a value of that protocol's type, and can't check for it dynamically (e.g., with is/as?).

It doesn't have to be limited to actors. For example, given something like:

func f() async {
  let object = MyClass()
  async let a1 = object.doSomething()
  async let s2 = object.doSomethingElse()
  await (a1, a2)
}

The object.doSomething() and object.doSomethingElse() expressions are invoked concurrently, and we could absolutely call this an error if object is not of a "shareable" type.

Doug

1 Like

I can't imagine any examples of this, can you share what you're thinking here?

I don't think there is any risk of over annotation in general - the proposal is providing the right tools for a type to say "I am already safe to transfer" by conforming to the protocol, and "I'm unsafe to transfer but do it anyway for this specific API". Any change to the type that makes it go from "unsafe to transfer in this release of the SDK" but "safe to transfer in the next version" seems like such an incredible ABI change that it would never be ABI compatible (even ignoring actor behavior).

Beyond that, my understanding is that adopting the imagined extensions (e.g. unique ownership etc) require fundamental redesign and reimplementations of the types in questions. I would expect old things to be replaced by new things when they roll out. Perhaps I'm just thinking of the wrong things.

But (at least for ValueSemantic), people will want to use it as a generic argument. I can also believe that there are cases for generics constrained by ActorTransferrable - the protocol doesn't provide any requirements so it isn't "useful" except preventing misuse with incompatible types, but that is what marker protocols are about.

I don't understand how you are imagining that this works: there are no actors, so there is no isolation, and therefore no memory safety. I haven't fully paged in the structured concurrency and nursery stuff, but async let must imply deep magic if this is creating concurrent tasks without an actor boundary between them. I'm a bit concerned about this as a prospect, but again, I haven't followed that line of development (I was waiting for rev #2 of the proposal).

If this ends up being a thing and if this proposal is useful to help with it, then I agree with you that we should pick a different word than Actor on ActorTransferrable, maybe ConcurrencyDomainTransferrable or something like that. I don't think that making the name fully generic like "sendable" (or some other verb) will work out well.

-Chris

I took a stab at this over in a new thread on preventing data races. It categorizes the various places where we have mutable state that can have data races (local variables, global/static variables, class instance variables) and how to deal with them. It also goes into how we can phase in the checking of these properties.

I was originally envisioning this as part of the Structured Concurrency proposal for "revision 2", but it belongs as a separate document even if parts of it get folded back into the proposal later.

Doug

2 Likes