Hello,
In my free time and as a part of a larger research project, I’ve been maintaining a proof-of-concept patch that implements strict value semantics. At this point in my project, I’d like to see what it would take to get strict value semantics integrated into mainline Swift.
What are strict value semantics?
Strict value semantics ensures that value types do not create or depend on side effects. This makes reasoning about the behavior of values easier, both for programmers and compiler optimizations. Strict value semantics also enables future language features. In my case, I’m researching a simple and efficient atomicity/concurrency/reentrancy model (but I’d like to not discuss it right now).
Casually speaking, strict value semantics means that value types “cannot” use reference semantics. Cannot is in quotes because reality is slightly complicated. For example, the standard library needs reference semantics to implement fundamental value types like collections. Also, value types continue use inout
internally to model mutating functions.
[EDIT] – Please note this patch is not proposing functional programming, therefore "strict" in this context does not referential transparency, etc.
Where to get the patch
On GitHub. Please note that until the standard library is built with strict value semantics enabled, there isn't anything end users can do to play with the feature outside of running only the type checker on test examples.
Details of the patch in progress
Functions now have an additional dimension of formal behavior: value semantic or reference semantic. This allows the compiler to ensure that value semantic functions cannot call reference semantic functions. This is the essence of the patch.
- Class methods/getters/setters/etc. default to “reference semantics”.
- Structs/enums methods/getters/setters/etc. default to “value semantics”.
- Protocols, by default, are semantic free. (See the next line.)
- Protocols need a way to guarantee reference or value semantics, otherwise strict value types cannot use any existential types. I’ve gone with “protocol Foo : !class” to mean a value semantic protocol, but reasonable people might dislike the syntax.
- Globals
- Global variables are a form of reference semantics. In this implementation of strict value semantics, strict value types can access global constants (“let” variables), but not mutable variables.
- Global functions need default semantics. Source compatibility would dictate that reference semantics should be the default. I think a case could be made that value semantics should be the default if source compatibility doesn’t matter. This opinion is based on contemporary programming conventions around value types and reference types; and reasonable people may disagree.
- An attribute exists to override the default semantics of a given function. For example, global functions. Not all functions can have their semantics overridden. For example, strict value types cannot have reference semantic methods.
- When it comes to function type conversion, value semantic functions implicitly convert to non-value-semantic functions (i.e. “reference semantics”). This may sound backwards, so think of it this way: a function that promises to never access reference semantics (i.e. a “strict value semantics” function) is convertible to function type with the same signature that don’t care (i.e. a “reference semantics” function).
Tradeoffs
- Closures created by strict value types or passed to strict value types cannot mutate captured variables because mutable captures are reference semantic and the closure must be value semantic in order to be callable by the strict value type. This change is incompatible with some programming conventions, but there are workarounds.
- I'm not sure what the right behavior is for "no escape" closures. We might be able to allow mutable captures if we're careful. On the other hand, this makes "no escape" closures be semantically different than escaping closures, which increases the complexity of language in an unfortunate way.
- Inout parameters are reference semantic. While I assume that the exclusivity checker is bug free, a case could be made that
inout
is a ObjC++ compatibility feature and that ObjC++ doesn’t have strict value types. Therefore we could just not allow strict value types to vend or use explicitinout
and therefore we could define away this case of reference semantics in a value semantic context. Also, one can use tuple return values instead ofinout
to mutate multiple values. For example, instead ofx = foo(&y)
, one can write(x, y) = foo(y)
. - Generic functions cannot infer their type and therefore they cannot infer value/reference semantics. As a consequence, programmers will need to duplicate code that needs to work with both semantic worlds. That being said (and practically speaking), people probably won’t do this, and the natural divide between reference types and value types will become more clear.
- What should be done about logging/debug APIs? Strictly speaking, these APIs create side effects and therefore strict value types cannot use these APIs. That being said and in practice, programs cannot rely on the side effects of logging/debug APIs and therefore I’d imagine that a new attribute could allow strict value types to call these kinds of APIs.
- Similarly, APIs like “random()” and system calls are not usable by strict value types. I’d argue that’s a good thing, but it may surprise people.
- Non-default property mutation. Mutating getters are getters with intentional side effects and therefore are incompatible with strict value semantics. Additionally, immutable setters are pointless no-ops under strict value semantics, because the setter cannot have side effects.
Work Remaining
- Turn strict value semantic errors into warnings if strict value semantics aren’t enabled.
- Add attribute to allow "safe/unobservable side effect" APIs (logging/debugging) to be callable from strict value types.
- Audit existing SIL optimizations. In particular, it is really easy for the compiler to drop
ExtInfo
bits. - Add new SIL optimizations that take advantage of strict value semantics.
- Compile the standard library with strict value semantics enabled.
- Compile miscellaneous libraries included with the compiler with strict value semantics enabled.
- Figure out the ObjC++ interoperability story. C structs do not have strict value semantics, so what should be done about C structs imports?
- Figure out the source compatibility story. Should people need to opt into strict value semantics? This can work, but the programming experience is like “const correctness” all over again.
Finally, and I cannot stress this enough, I wholeheartedly believe that strict value semantics is a prerequisite to simple and efficient concurrency models. I hope that we can make this feature happen.
Cheers,
Dave