Pitch #2: Protocol-based Actor Isolation

Another example - keeping track of preferences for multiple users

import Foundation
struct Settings {
    let user: String
    var lightMode: Bool {
        get {
            UserDefaults.standard.bool(forKey: user+"_lightMode")
        }
        nonmutating set {
            UserDefaults.standard.setValue(newValue, forKey: user+"_lightMode")
        }
    }
    var indentationSize: Int {
        get {
            UserDefaults.standard.integer(forKey: user+"_indentationSize")
        }
        nonmutating set {
            UserDefaults.standard.setValue(newValue, forKey: user+"_indentationSize")

        }
    }
}

let settings = Settings(user: "John")
settings.lightMode = false
let copy = settings
copy.lightMode = true
print(settings.lightMode) // true

I expanded on what this could look like here: thoughts appreciated @gribozavr.

Also @cukr, I included your point about closure captures in the closure section. Thanks!

I think that there are three reasonable ways to disable implicit ValueSemantic conformance:

  1. Switch to work like Hashable and Equatable, requiring an explicit conformance for autosynth.
  2. Introduce an attribute like @noValueSemanticAutoSynthesis.
  3. Introduce an attribute like @disableImplicit(x) where x could be things like ValueSemantic, memberwise init, and/or implicit copy constructor when we have move semantic types.

Any thoughts or opinions on this?

-Chris

2 Likes

What behavior do we want for public types? The choice Swift generally makes is that all public API contracts must be explicitly stated. That convention implies that ValueSemantic conformance must be explicit for public types. If that is the case there is at least some incentive to require explicit conformance everywhere. That said, if we do go with implicit conformance I think option 3 is the best choice.

Most value semantic types already conform to Equatable and usually Hashable. Explicit conformance could be made more convenient if we introduced a couple typealiases:

typealias EquatableValue = Equatable & ValueSemantic
typealias HashableValue = Hashable & ValueSemantic

This is something users could easily add themselves if we don’t’ want to put these in the standard library.

2 Likes

I agree about auto-synthesized Equatable and Hashable conformances potentially being incorrect, but they should be memory-safe.

I like it. This idea mirrors Rust's unsafe traits design.

We could also borrow a page from Rust, specifically the PhantomData type. The type that wants to opt out can add a PhantomData<AnyObject> property -- the authosynthesis would see a non-value type stored var and would avoid synthesizing the conformance (but since the property is zero-size it does not cost anything at runtime). That way we can keep synthesis rules uniform (no opt-out processing).

1 Like

Really great point, brings back all the memories of you and I wrestling with memberwise init. :cry:

I'm generally uncomfortable with either of these being implicit (I'd personally rather them be explicit opt-in like hashable), but I think that scoping them to internal types only makes me a lot more comfortable with the implicit behavior. This is a great suggestion. I'll incorporate it into the proposal.

Yeah, I think we should evaluate something like this as a follow-on. They are synactic sugar and are likely to draw a lot of debate that detracts from the core proposal. I'll mention them in 'future directions' though, thanks!

-Chris

Interesting idea. It isn't really related to this proposal, but something that I would like to explore in the future is an attribute that turns off all autogenerated members -- including the default assignment operator, destructor for structs, and move/moveinit hooks in the basic type witness table. Swift internally supports arbitrary logic here, but we don't allow advanced users to write their own types with their own implementation of these hooks. I'd love to be able to write something like (disclaimer: I made no attempt to pick good names here :slight_smile:):

@explicit
struct MySmartPointer {
   init(_ x: MySmartPointer) { 
     // custom copy ctor 
   }

  init(_ x: ^MySmartPointer) { 
     // move constructor.
  }

  operator=(_ x: MySmartPointer) { 
     // custom copy reassign operator
  }

  operator=(_ x: ^MySmartPointer) { 
     // custom move reassign.
  }

  deinit {
     // custom destructor
  }
}

In addition to being valuable for advanced pure-swift APIs, such a thing is useful when you need to write a C API in Swift that has init/copyctor/dtor hooks. Currently the least-bad thing to do is use a class for this, which adds an extra level of indirection and a memory allocation.

This could also be way to handle move only types and other weird things that C++ can express that Swift can't.

-Chris

10 Likes

This would make the interaction from Swift to the C much less expensive. I wonder what only a simple 'deinit' destructor on structs would do.
Having to wrap heap allocated C handles (which are cheap to just copy around) on classes and incurring in unneeded ref-counting its just taxing the performance without need.

BTW: Given its all LLVM, the abi on both sides are stable, etc.. would be possible to get a move from the C++ world to Swift AND give the possibility for Swift to automatically call the C++ destructor in case the handle is scoped destruct?? (As this combo would turn feasible to avoid unneeded heap allocations on C++ side)

With 'deinit' in structs, moves and C++ object destruction, it would be such a game changer for Swift, giving it would become the best language to interface with C and C++, which we all know, have wonderful codebases that are important to interface with (with LLVM being one of them).

I'm sure you have a much better idea, a clear picture for all this, giving the wizard you are, but i'm just trying to make it clear how important a couple of changes in the Swift language would do to the development of the ecosystem around it.

1 Like

I would add that if we can do this, then "let's" pull the HeapObject single threaded definition inside swift runtime into a separate class, so we can rig non-atomic, non-locking swift allocations to this phantom stuff, and we can build compiler safe concurrency first class into the language. Quite the perf boost!

heapobject in swift runtime

1 Like

Oh yeah, and we need to be able to override malloc too :-)

I've been thinking more about explicit conformances and the explicit "unsafe" annotation you mention. In particular, with explicit conformance being required for public types, we now have no signal to the programmer that they are taking responsibility for correct conformance. I think we need a signal here, especially for public members.

With this in mind, I think it would be good to require @unsafe ValueSemenatic in explicit conformances when the conformance does not follow trivially from composition of stored values. When all stored values conform, a normal ValueSemantic conformance would be accepted (and necessary for public types). This is similar to, but slightly different than, the @unsafeConformance approach you describe.

I think this proposal will have some impact on the "stored properties in extensions" idea that had been discussed many times in the forums. The latest (short) discussion was from 4 months ago:

For example, if String conforms to ActorSendable, then it would be an error to extend it with a stored reference-type property:

extension String {
    let newStoredProperty: NSMutableString = "a mutable string"
}

"Stored properties in extensions" is not a Swift feature, and definitely outside of the scope of this proposal, but I think it might be beneficial to discuss about this proposal's impact on it.

1 Like

On the topic of ValueSemantic vs UnsafeValueSemantic, maybe we can have them both by making ValueSemantic a subset of UnsafeValueSemantic.

As far as I can see, there are 2 situations where conformance to ValueSemantic is unsafe:

  1. When the conforming type is a reference type.

  2. When the conforming type is a value type, but it's not composed of only ValueSemantic types.

Situation 2 can be addressed by an error message when the compiler cannot auto-synthesise the conformance. Situation 1 can be addressed if we have a way to mark value types like how we mark reference types with AnyObject:

Maybe we can introduce another new protocol AnyValue (or ValueType or something else, bikesheddable) opposite to AnyObject, and have all value types (structs, enums, tuples) implicitly conform to it. Then, we can allow UnsafeValueSemantic written as ValueSemantic when the conforming type is a reference type:

typealias ValueSemantic = UnsafeValueSemantic where Self: AnyValue

^ This expression isn't possible in Swift today, though. So some additional compiler magic might be needed.

Alternatively, it might be more workable if ValueSemantic inherits from both AnyValue and UnsafeValueSemantic:

protocol ValueSemantic: AnyValue, UnsafeValueSemantic {}

class Foo: ValueSemantic { // error: Non-value-type 'Foo' cannot conform to value-type protocol 'ValueSemantic'
                           // fix-it: replace 'ValueSemantic' with 'UnsafeValueSemantic'
    let bar: Int
}

Or, a third option, make ValueSemantic a composition of AnyValue and UnsafeValueSemantic:

typealias ValueSemantic = AnyValue & UnsafeValueSemantic

class Foo: ValueSemantic { // error: Inheritance from non-protocol type 'ValueSemantic' (aka 'AnyValue & UnsafeValueSemantic')
                           // fix-it: replace 'ValueSemantic' with 'UnsafeValueSemantic'
    let bar: Int
}

^ These latter 2 options also need some additional compiler magic, but it's precedented by AnyObject.

FWIW, I think that many of the comments in this thread are pushing towards making ValueSemantic conformance always be explicit, instead of ever being implicitly synthesized. The current proposal (implicit synthesis for non-public types) is a bit of a middle ground, but doesn't seem very satisfactory.

It seems that following the precedent of Hashable and Codable synthesization would be cleaner. WDYT?

1 Like

Making conformance explicit would be particularly helpful when making changes to existing types (eg adding a new field). It’s currently very helpful to be told immediately that a new field you’ve just added stops automatic Equatable/Codable conformance. I would imagine it would be the same for ValueSemantic.

Otherwise, I think people will be in the situation where passing a parameter of a particular type to an Actor works (without them doing anything, or maybe even knowing why it works), they make a change and an error appears some distance away.

I think the consistency argument also has a lot of merit.

1 Like

“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.