Got it. Oh, this is fun!
We're going to need to agree on some terminology, though. That is what I call âinitialization,â not âassignment.â The difference is that initialization creates a scope in which a name is bound to a value, but assignment is an inherently mutating operation. Depending on your point of view, you might think of that as mutating a stored value in-place, or as mutating the binding of a variable within a given scope (more on those two points of view below):
var x = 3 // initialization
x = 4 // assignment
These names come from my background with C++ (which I know is broadly eschewed in PL research). If you have other words for these distinctions, I'm happy to consider using those instead.
I did not
. Many PL researchers, myself included, are in a never ending quest for type soundness, following the "well typed cannot go wrong" slogan from Robin Milner.
In more concrete terms, I would like to show that if my type system says "OK, your program looks fine", then it cannot possibly crash at runtime (except for an explicitly unsafe operation, like x as! T ).
Yes, I'm well aware. Describing value semantics doesn't depend in any way on having the ability to express type- or memory-unsafe computations, so I think that might be a somewhat orthogonal issue.
In my view those things are inextricably linked. Unless we needlessly attach a notion of identity or address to values, the only way to observe aliasing is if there is mutation. Conversely, since identity is uninteresting in the absence of mutation, I don't see any motivation for including identity in the model.
Note: in the absence of identity and mutation, there's no way to distinguish reference semantics from value semantics. Because of the crucial role of mutation in making the distinction interesting, for some audiences, I've taken to calling value semantics âmutable value semantics.â
I might be misunderstanding something. I might be confused by the use of the term variable . In let x = 42 , would you consider x to be one?
Calling a let binding a âvariableâ is not incompatible with what I mean, but for the purposes of my description of requirements, which is designed to help a programmer ascertain whether a given type has value semantics, it's important to consider things that can happen when x is a var binding. So the simple answer is âno.â The more nuanced answer is âyes, but let bindings are a special case of variables that don't allow us to fully explore whether a type has value semantics.â
I prefer the term binding , as defined in Swift's language reference . This enables a clear distinction between let (a.k.a. constant) and var (a.k.a. variable or mutable) bindings.
OK, so this is where the two points-of-view come into play. There are two equivalent, simple mental models for thinking about mutable value semantics, where the word âvariableâ here includes names defined with both var and let:
- Variables are (potentially mutably) âbound toâ immutable values. Initializations, assignments, and pass-by-value implicitly copy the binding, so two variables can be thought of as being bound to the âsameâ value. However, because the values are immutable and there's no identity, âsame valueâ is meaningless apart from differences that could be distinguished by an
== operation.
- Variables are âcontainers ofâ a single (potentially-mutable) value. Initializations, assignments, and pass-by-value implicitly create a copy of the entire contained value of the source.
The models are interchangeable for the purposes of describing operational semantics (neither one captures the computational complexity of operations on every type with value semantics). The design of Swift used the second model, in part because it allows us to describe references as values that refer to stored class instances, and because it maps more naturally to aggregate structures that aren't full of pointers.
Under this interpretation, the first bullet holds in the absence of mutation, whereas the independant part would be threatened by mutation.
As noted above, value semantics is not even interesting in the absence of mutation.
We might be in complete agreement, but simply attributing different definitions to the term "reference". Perhaps the phrasing "they are predicated upon a notion of reference aliasing" would be better suited?
I don't think so. If you're trying to say that absent the possibility of aliasing, value semantics aren't worth describing, because there's nothing else in the universe, I could understand that, but I would still disagree. That would be like saying integers have no properties worth formalizing in a universe that doesn't include fractions.
The 2nd to last bullet mentions, as one of the exceptions: "passing the expression as an inout parameter." This suggests that, at some point of the execution, there must be two bindings for the same value, two points of access .
This sounds like a misunderstanding of inoutâŚ
The law of exclusivity guarantees that there is no aliasing. The simplest way to look at a function taking an inout parameter is in terms of a rewrite rule fâf', in which each inout parameter to f is passed by value to f' and âtuple-izedâ into the return value of f', and a corresponding rule for the use-sites of f that assigns into the inout variable. So your code (which would be a little more illustrative if it actually did some mutation) could be rewritten as:
func fPrime(x: Int) -> (Void, x: Int) {
let b = x
return ((), x: x)
}
var a = 26
(_, a) = fPrime(x: a)
You can either look at this as:
- the value contained in
a is copied into the parameter x and which is copied into the last element of the returned tuple, which is then copied back into a, or
- the parameter
x is bound to the value a is bound to; the last element of the returned tuple is bound to value x is bound, and finally a is rebound to the value that the last element of the returned tuple is bound to.
When Swift gets noncopyable types (and officially gets _modify accessors), the mental model has to change slightly: the value of the inout parameter is âexclusively borrowedâ for the duration of the call to f (see Rust). Either way, exclusivity guarantees that there's no aliasing.
Both observations suggest the existence of a mapping that keeps track of the current bindings in the program.
That sounds like it's just restating the âbindingâ point of view for languages that have mutation: if you are going to think of all mutation as rebinding variables, then the state of the program is captured in the set of current variable bindings.
I'll try to grok the rest of your post while you read this 