ValueSemantic protocol

I think value semantics are best summarized by an out-of-place transform function:

func transformedCopy<T>(
  _ x: T, transform: (inout T)->()
) -> T {
  var result = x
  transform(&result)
  return result
}

That implementation is correct if and only if T has value semantics.

• • •

More concretely, a type T has value semantics if and only if, for all possible instances of T and all possible transform operations (edit: which do not mutate anything other than the inout argument), the use of transformedCopy does not modify its argument.

Thus, in the following snippet:

var x: T = ...
// 1
let y = transformedCopy(x){ ... }
// 2

If at lines 1 and 2 we print out a complete description of all salient features of x, and they are not identical to each other, then T does not have value semantics.

In particular, if there exists any possible x and any possible transform (edit: same caveat as above) for which they are different, then the type does not have value semantics.

• • •

Edit:

Of course, if transform captures and mutates the variable which is being passed in as x then we get the same situation as in MutableReference above, so we ought to rule that out.

But even if it doesn’t, we can still get situations like this:

var a: Int = 1

var b: Int {
  get { a }
  set { a = newValue }
}

print(b) // 1

let c = transformedCopy(b){
  a += 1
  $0 += a
}

print(b) // 2

So indeed the definition is quite slippery.

I do, however, still maintain that the implementation of transformedCopy is correct if and only if T has value semantics. Indeed, functions like that are exactly and precisely where I would want to use a T: ValueSemantics constraint.

1 Like

So you prefer to think it is some kind of mathematical equivalence taking every time it is called the closure argument a again into it.

Well this isn't the case for closures in general.
But for now assume this is true.
When a is passed, why does setting a in callee scope mutate the a in the caller scope, passing value a would normally allocate a new resource or reuse some resource which isn't accessed mutably by other vars.
So mutating a in the callee shouldn't cause an effect in the caller scope unless we make it an out argument, but that would prohibit mutable shared access to other threads which I think is still possible with Swift closures.

I think a type with value semantic is a type which forces all variables bounded by this type to follow value semantics such that passing an instance of that type to a function mutating its first arguments is safe as the type forces the resource system to copy (parts of) the instance (lazily) in case a mutation occurs.

What do you mean by “covers exactly?”

We can effectively pass structs by “plain reference” just by using an UnsafePointer (or a wrapped one) as a proxy. If that possibility breaks the idea that a type has value semantics, then the ability to form an UnsafePointer to a threadsafe queue breaks the idea that you can build a threadsafe type.

Unsafe operations aside, I don't understand how your claim is more meaningful than it would be to say “talking about immutable instances is a failure, because if we were allowed to mutate a let binding we'd undermine the concept of immutability,” which is clearly absurd. To me, your claim seems just as tautological. The language puts up certain guardrails that allow us (unsafe operations aside) to guarantee properties of the entities we define, be they types or instances. Not allowing you to form a reference to a value type instance is one of them.

A type shouldn't provide value semantics, instead variables should do that with appropriate annotations/keywords.

Well, that's an interesting idea certainly, but:

  1. It seems rather speculative; is there a language that reifies that idea?
  2. If you don't tie value semantics to type, you are describing a very different concept than those of us who have been talking about (mutable) value semantics for many years. It may be a useful concept (maybe even more useful!) and is probably related, but I suggest for the sake of everyone's understanding that you pick different terminology.

The motivation behind the exercise of defining value semantics here was to capture what those of us discussing value semantics as a property of types for many years have meant, in a way that is useful and formalizable. If you are claiming that the definition I've offered doesn't do that, I'd like to better understand your reasons for saying so.

2 Likes

Okay, maybe a bit exaggerated, but I think it state quite well the point what value semantics are, they are all about treating variables as constants for values.

I would exclude unsafe operations out of consideration as they introduce a possible axiom break in the type system, i.e. they introduce an assumption which may not hold.
Yes unsafe keywords belong to the language but they represent a backdoor behind type system guarantees.

Good point, but would you say a let is a let if we directly mutate it like a var?
And would you say that a struct is a struct if we mutate it?

I think in case of the latter, this makes sense, but yeah, given the current semantics in Swift it may not be a struct anymore.

I think Swift already did this to some extent with inout, likewise in D. D has also the immutable keyword making a deep copy of any type.
Julia doesn't differentiate at all between value and reference types, all things are objects, you have structs for classes and structs, but still they tie (im)mutability to types.

We can for sure consider value semantic types, but their value-semantics strongly depends on (future) keywords.

My concerns:

Did your definition exclude Int to be a value type?
Most people would consider Int to follow Value Semantics, and now an integer variable changes because a closure changes this var in an unexpected way because the said closure is executed in a threaded context.

Even if we currently have a type following value semantics, does an addition of a keyword for variables break the conformance to the Value Semantic Protocol in the future, the other way around, is the evolution of Swift hindered with the introduction of the Value Semantic Protocol in adding new keywords for variables?

I'm not sure I understand what you mean, but on the face of it, this sounds like you mean something very different from what I mean when you say “value semantics.” For value semantics as I intend it to be interesting, mutation is required, so AFAICT it has nothing to do with treating anything as constant.

I disagree, and this goes directly to the nature of “value semantics” as I intend the phrase. A meaningful definition is not dependent on having language guarantees, just as Collection has meaning that can't be enforced by language mechanisms.

In fact, one of the most important points is that we can create types with value semantics by properly combining types that don't have value semantics. Any useful definition of value semantics has to allow for the creation of types like Swift's Array<Int>, which uses a reference type under the covers. It's possible there's a collection of keywords and language features that simultaneously prevents all non-value-semantic use of reference types while allowing their use in all ways that preserve value semantics, but I'm not sure how we'd prove to ourselves that we'd actually found one. I am even less sure that—even if we can find such a collection—it would be worth adding to the language, in terms of complexity cost.

None of that is to say we shouldn't try; it would be really interesting to see the result. But in the meantime, we have all these types whose variables have an independent, well-defined value, and there are all kinds of conclusions we can draw about thread-safety, side-effects, and local reasoning on that basis. My definition addresses that reality.

Yes, Int is a value type by my definition. The fact that you can touch the same mutable Int value via multiple access paths when it is captured in a closure is not news nor is it a problem for my definition; the same thing happens when the Int is a stored property of a class instance. Closures and class instances have reference semantics in Swift. It's rather unfortunate, IMO, that Swift closures implicitly capture by reference, making it all too easy to cause a problem unintentionally. IIUC there was supposed to be some work to address that in connection with the concurrency efforts, but I don't know what it is or how it's been realized.

2 Likes

Damn, I wanted to ask if Int is a value semantic type. Following your definition this seems to be true.
But I don't think this is the case.

Puh, then we might talk past each other, treating anything as constant is only at abstraction, mutability or not for a operation on a constant + rebinding is just an implementation detail and solved by the backend then.

I'm referring to the vague definition of wikipedia. A more common term is referential transparency

Edit:

After long thinking, we need some point of clarification.

Does your definition of a value semantic type implies value semantic behavior for all declared variables of the said type?

What would go wrong if Siesta resource is a reference type?

In SwiftUI, I used State to hold optional reference to reference type, because it needed to be created and destroyed dynamically. In this case observing changes in identity are separated from observing changes in object state. Parent view is responsible for the former, why child views are responsible for the latter. Contract is pretty clear, and as long as all parties respect it - everything works well. It’s a somewhat exotic case, but IMO, not an anti-pattern.

Resources in Siesta are reference type, very close to being main thread actors in the new regime. It’s the resource payload that’s the concern. Two things:

  1. The resource payload gets passed across thread boundaries. For this, the newly minted Sendable would suffice.
  2. Resource observers will want to know what the content of the payload changes. For this, you truly do want value semantics, not just Sendable. One could finagle around it, but it does break the expectation of “if the state changes, views of that state will find out about it” that underlies all these reactive-programming-shaped approaches. The same applies in your SwiftUI case, I think.

Based on how Swift handles mutability of the class references, I agree with @Joe_Groff that “value” of the class type includes only object’s identity, and does not include object’s stored properties.

let declarations should have immutable value. Thus everything that can be mutated through let-variable is not a part of the value.

It is possible to have different mental model, but that means not benefiting from compiler’s mutability analysis. Which is a pretty high penalty, IMO.

Based on this definition of value, Swift already provides a protocol for semi-regular types, and it is Any. If value semantics means semi-regular type which can cross thread isolation boundaries, then ValueSemantics = Any & ActorSendable = ActorSendable.

I don’t think ValueSemantics is useful as a marker protocol, without any members (or any members in addition to ActorSendable). If protocol is empty, this means that the only thing that generic code does with values is just passing them around. Technically, even Any is sufficient for that.

Maybe instead it would be more useful to be able to write a protocol which declares some members and specifies if they are part of the value or not.

We already have a small tool to make a property not part of the value. If setter is marked as nonmutating by the logic above this excludes setter from the value.

I have a vague idea, that maybe this could be extended to declare that property is a part of the value. Currently we by default setters are mutating in the protocol, but protocol requirement will be satisfied by the non-mutating setter as well (all setters in classes are non-mutating). Maybe we need a new modifier, something like mustmutate, disallowing non-mutating implementations.

For read-only properties, it gets more complicated, we need to express “I don’t require a setter, but if you provide one, it must be mutating”.

Maybe both could be combined into a single modifier value.

Something like this:

protocol P {
    value var foo: Foo { get }
    value var bar: Bar { get set }
    var baz: Baz { get }
    func addBazObserver(observer: @escaping () -> Void) -> AnyCancellable
}

class User<T: P> {
    var item: T {
        didSet {
            // I can be confident that this is the only point where changes in foo and bar need to be handled.
            // But I know that with baz it’s more complicated.
        }
    }
}

It's not that cut-and-dried. It's easy to define an immutable final class whose value includes the stored properties; such types have perfectly well-behaved value semantics.

There's also the problem that the value of a type has to do with the meaning of that type; in general, it's not mechanically derivable by looking at source code. In practice, people have written (mutable and/or subclassable) classes and given them Equatable conformances based on the stored properties. However ill-conceived, these types do have a notional value that includes their stored properties.

Based on this definition of value, Swift already provides a protocol for semi-regular types, and it is Any .

As noted in my definition of value semantics, it is true that any type can be said to have value semantics, depending on how we define its value. So it's legitimate, in a sense to say “every type has value semantics” and “Any is the protocol for values”… it's just… not useful. A distinction that includes everything has no meaning.

I don’t think ValueSemantics is useful as a marker protocol, without any members (or any members in addition to ActorSendable). If protocol is empty, this means that the only thing that generic code does with values is just passing them around. Technically, even Any is sufficient for that.

The reason you can't find a set of required operations for value semantics (the hard, local reasoning, part) is that it is a purely semantic distinction, and what it specifies is a particular relationship between pass-by-value/assignment/initialization and all mutating operations on the type, whatever they may be.

Being purely semantic would not make a ValueSemantic protocol useless; this is probably the simplest generic algorithm you could write that depends on this relationship:

extension ValueSemantic {
  /// Returns a copy of `self`, with `mutator` applied.
  func mutated(by mutator: (inout Element)->Void) -> Self {
   var x = self
   mutator(&x)
   return x
  }
}

That said, I would be happy to have a ValueSemantic protocol refine Equatable or maybe Hashable, so it needn't lack syntactic requirements.

Maybe instead it would be more useful to be able to write a protocol which declares some members and specifies if they are part of the value or not.

That's a fascinating thought… I can't quite see how it would play out, though.

We already have a small tool to make a property not part of the value. If setter is marked as nonmutating by the logic above this excludes setter from the value.

That is true, but some properties that are not part of the value are read-only, like the capacity of an Array. And if we imagine that it could have a setter, the only way to implement that setter would be by mutating the Array… so nonmutating would be prohibited by the compiler.

3 Likes

In case anyone is interested, a paper that (among other things) defines “mutable value semantics” has been accepted into the Journal of Object Technology ‘s upcoming issue. A preprint is available here .

(correcting link and deleting the original post, thanks @David_Sweeris !)

19 Likes