"Fossies" - the Fresh Open Source Software Archive

Member "swift-swift-5.1.2-RELEASE/docs/proposals/valref.rst" (7 Nov 2019, 21588 Bytes) of package /linux/misc/swift-swift-5.1.2-RELEASE.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format (assuming markdown format). Alternatively you can here view or download the uninterpreted source code file. A member file download can also be achieved by clicking within a package contents listing on the according byte size field. See also the last Fossies "Diffs" side-by-side code changes report for "valref.rst": 5.0.3_vs_5.1.

orphan

Values and References

Author

Dave Abrahams

Author

Joe Groff

Date

2013-03-15

Abstract: We propose a system that offers first-class support for both value and reference semantics. By allowing--but not requiring--(instance) variables, function parameters, and generic constraints to be declared as val or ref, we offer users the ability to nail down semantics to the desired degree without compromising ease of use.

Note

We are aware of some issues with naming of these new keywords; to avoid chaos we discuss alternative spelling schemes in a Bikeshed section at the end of this document.

Introduction

Until recently, Swift's support for value semantics outside trivial types like scalars and immutable strings has been weak. While the recent Clonable proposal makes new things possible in the "safe" zone, it leaves the language syntactically and semantically lumpy, keeping interactions between value and reference types firmly outside the "easy" zone and failing to address the issue of generic programming.

This proposal builds on the Clonable proposal to create a more uniform, flexible, and interoperable type system while solving the generic programming problem and expanding the "easy" zone.

General Description

The general rule we propose is that most places where you can write var in today's swift, and also on function parameters, you can write val or ref to request value or reference semantics, respectively. Writing var requests the default semantics for a given type. Non-class types (structs, tuples, arrays, unions) default to val semantics, while classes default to ref semantics. The types val SomeClass and ref SomeStruct also become part of the type system and can be used as generic parameters or as parts of tuple, array, and function types.

Because the current specification already describes the default behaviors, we will restrict ourselves to discussing the new combinations, such as struct variables declared with ref and class variables declared with val, and interactions between the two.

Terminology

When we use the term "copy" for non-class types, we are talking about what traditionally happens on assignment and pass-by-value. When applied to class types, "copy" means to call the clone() method, which is generated by the compiler when the user has explicitly declared conformance to the Clonable protocol.

When we refer to variables being "declared val" or "declared ref", we mean to include the case of equivalent declarations using var that request the default semantics for the type.

Unless otherwise specified, we discuss implementation details such as "allocated on the heap" as a way of describing operational semantics, with the understanding that semantics-preserving optimizations are always allowed.

When we refer to the "value" of a class, we mean the combination of values of its val instance variables and the identities of its ref instance variables.

Variables

Variables can be explicitly declared val or ref:

var x: Int  // x is stored by value
val y: Int  // just like "var y: Int"
ref z: Int  // z is allocated on the heap.

var q: SomeClass          // a reference to SomeClass
ref r: SomeClass          // just like "var r: SomeClass"
val s: SomeClonableClass // a unique value of SomeClonableClass type

Assignments and initializations involving at least one val result in a copy. Creating a ref from a val copies into heap memory:

ref z2 = x         // z2 is a copy of x's value on the heap
y = z              // z2's value is copied into y

ref z2 = z         // z and z2 refer to the same Int value
ref z3 = z.clone() // z3 refers to a copy of z's value

val t = r          // Illegal unless SomeClass is Clonable
ref u = s          // s's value is copied into u
val v = s          // s's value is copied into v

Standalone Types

val- or ref-ness is part of the type. When the type appears without a variable name, it can be written this way:

ref Int                 // an Int on the heap
val SomeClonableClass  // a value of SomeClonableClass type

Therefore, although it is not recommended style, we can also write:

var y: val Int               // just like "var y: Int"
var z: ref Int               // z is allocated on the heap.
var s: val SomeClonableClass // a unique value of type SomeClonableClass

Instance Variables

Instance variables can be explicitly declared val or ref:

struct Foo {
    var x: Int  // x is stored by-value
    val y: Int  // just like "var y: Int"
    ref z: Int  // allocate z on the heap

    var q: SomeClass          // q is a reference to SomeClass
    ref r: SomeClass          // just like "var r: SomeClass"
    val s: SomeClonableClass // clone() s when Foo is copied
}

class Bar : Clonable {
    var x: Int  // x is stored by-value
    val y: Int  // just like "var y: Int"
    ref z: Int  // allocate z on the heap

    var q: SomeClass          // q is stored by-reference
    ref r: SomeClass          // just like "var r: SomeClass"
    val s: SomeClonableClass // clone() s when Bar is clone()d
}

When a value is copied, all of its instance variables declared val (implicitly or explicitly) are copied. Instance variables declared ref merely have their reference counts incremented (i.e. the reference is copied). Therefore, when the defaults are in play, the semantic rules already defined for Swift are preserved.

The new rules are as follows:

Arrays

TODO: reconsider sugared array syntax. Maybe val<Int>[42] would be better

Array elements can be explicitly declared val or ref:

var x : Int[42]         // an array of 42 integers
var y : Int[val 42]     // an array of 42 integers
var z : Int[ref 42]     // an array of 42 integers-on-the-heap
var z : Int[ref 2][42]  // an array of 2 references to arrays
ref a : Int[42]         // a reference to an array of 42 integers

When a reference to an array appears without a variable name, it can be written using the usual syntax:

var f : () -> ref Int[42] // a closure returning a reference to an array
var b : ref Int[42]       // equivalent to "ref b : Int[42]"

Presumably there is also some fully-desugared syntax using angle brackets, that most users will never touch, e.g.:

var x : Array<Int,42>               // an array of 42 integers
var y : Array<val Int,42>           // an array of 42 integers
var z : Array<ref Int,42>           // an array of 42 integers-on-the-heap
var z : Array<ref Array<Int,42>, 2> // an array of 2 references to arrays
ref a : Array<Int,42>               // a reference to an array of 42 integers
var f : () -> ref Array<Int,42>     // a closure returning a reference to an array
var b : ref Array<Int,42>           // equivalent to "ref b : Int[42]"

Rules for copying array elements follow those of instance variables.

unions

Union types, like structs, have default value semantics. Constructors for the union can declare the val- or ref-ness of their associated values, using the same syntax as function parameters, described below:

union Foo {
  case Bar(ref bar:Int)
  case Bas(val bas:SomeClass)
}

Unions allow the definition of recursive types. A constructor for a union may recursively reference the union as a member; the necessary indirection and heap allocation of the recursive data structure is implicit and has value semantics:

// A list with value semantics--copying the list recursively copies the
// entire list
union List<T> {
  case Nil()
  case Cons(car:T, cdr:List<T>)
}

// A list node with reference semantics--copying the node creates a node
// that shares structure with the tail of the list
union Node<T> {
  case Nil()
  case Cons(car:T, ref cdr:Node<T>)
}

A special union type is the nullable type T?, which is sugar syntax for a generic union type Nullable<T>. Since both nullable refs and refs-that-are-nullable are useful, we could provide sugar syntax for both to avoid requiring parens:

ref? Int // Nullable reference to Int: Nullable<ref T>
ref Int? // Reference to nullable Int: ref Nullable<T>
val? SomeClass // Nullable SomeClass value: Nullable<val T>
val Int? // nullable Int: val Nullable<T> -- the default for Nullable<T>

Function Parameters

Function parameters can be explicitly declared val, or ref:

func baz(
    _ x: Int      // x is passed by-value
  , val y: Int  // just like "y: Int"
  , ref z: Int  // allocate z on the heap

  , q: SomeClass               // passing a reference
  , ref r: SomeClass           // just like "var r: SomeClass"
  , val s: SomeClonableClass) // Passing a copy of the argument

Note

We suggest allowing explicit var function parameters for uniformity.

Semantics of passing arguments to functions follow those of assignments and initializations: when a val is involved, the argument value is copied.

Note

We believe that [inout] is an independent concept and still very much needed, even with an explicit ref keyword. See also the Bikeshed discussion at the end of this document.

Generics

TODO: Why do we need these constraints? TODO: Consider generic classes/structs

As with an array's element type, a generic type parameter can also be bound to a ref or a val type.

var rv = new Array<ref Int> // Create a vector of Ints-on-the-heap var vv = new Array<val SomeClass> // Create a vector that owns its SomeClasses

The rules for declarations in terms of ref or val types are that an explicit val or ref overrides any val- or ref-ness of the type parameter, as follows:

ref x : T // always declares a ref
val x : T // always declares a val
var x : T // declares a val iff T is a val

ref and val can be specified as protocol constraints for type parameters:

// Fill an array with independent copies of x
func fill<T:val>(_ array:[T], x:T) {
  for i in 0...array.length {
    array[i] = x
  }
}

Protocols similarly can inherit from val or ref constraints, to require conforming types to have the specified semantics:

protocol Disposable : ref {
  func dispose()
}

The ability to explicitly declare val and ref allow us to smooth out behavioral differences between value and reference types where they could affect the correctness of algorithms. The continued existence of var allows value-agnostic generic algorithms, such as swap, to go on working as before.

Non-Copyability

A non-Clonable class is not copyable. That leaves us with several options:

  1. Make it illegal to declare a non-copyable val
  2. Make non-copyable vals legal, but not copyable, thus infecting their enclosing object with non-copyability.
  3. Like #2, but also formalize move semantics. All vals, including non-copyable ones, would be explicitly movable. Generic var parameters would probably be treated as movable but non-copyable.

We favor taking all three steps, but it's useful to know that there are valid stopping points along the way.

Default Initialization of ref

TODO

Array

TODO: Int[...], etc.

Equality and Identity

TODO

Why Expand the Type System?

TODO

Why do We Need [inout] if we have ref?

TODO

Why Does the Outer Qualifier Win?

TODO

Objective-C Interoperability

Clonable Objective-C classes

In Cocoa, a notion similar to cloneability is captured in the NSCopying and NSMutableCopying protocols, and a notion similar to val instance variables is captured by the behavior of (copy) properties. However, there are some behavioral and semantic differences that need to be taken into account. NSCopying and NSMutableCopying are entangled with Foundation's idiosyncratic management of container mutability: -[NSMutableThing copy] produces a freshly copied immutable NSThing, whereas -[NSThing copy] returns the same object back if the receiver is already immutable. -[NSMutableThing mutableCopy] and -[NSThing mutableCopy] both return a freshly copied NSMutableThing. In order to avoid requiring special case Foundation-specific knowledge of whether class types are notionally immutable or mutable, we propose this first-draft approach to mapping the Cocoa concepts to Clonable:

This is suboptimal for immutable types, but should work for any Cocoa class that fulfills the NSMutableCopying or NSCopying contracts without requiring knowledge of the intended semantics of the class beyond what the compiler can see.

Objective-C (copy) properties should behave closely enough to Swift val properties to be able to vend Objective-C (copy) properties to Swift as val properties, and vice versa.

Objective-C protocols

In Objective-C, only classes can conform to protocols, and the This type is thus presumed to have references semantics. Swift protocols imported from Objective-C or declared as [objc] could be conformed to by val types, but doing so would need to incur an implicit copy to the heap to create a ref value to conform to the protocol.

How This Design Improves Swift

  1. You can choose semantics at the point of use. The designer of a type doesn't know whether you will want to use it via a reference; she can only guess. You might want to share a reference to a struct, tuple, etc. You might want some class type to be a component of the value of some other type. We allow that, without requiring awkward explicit wrapping, and without discarding the obvious defaults for types that have them.
  2. We provide a continuum of strictness in which to program. If you're writing a script, you can go with var everywhere: don't worry; be happy. If you're writing a large-scale program and want to be very sure of what you're getting, you can forbid var except in carefully-vetted generic functions. The choice is yours.
  3. We allow generic programmers to avoid subtle semantic errors by explicitly specifying value or reference semantics where it matters.
  4. We move the cases where values and references interact much closer to, and arguably into, the "easy" zone.

How This Design Beats Rust/C++/C#/etc.

structs Really Should Have Value Semantics

It is possible to build a struct with reference semantics. For example,

..parsed-literal:

struct XPair
{
   constructor() {
       // These Xs are notionally **part of my value**
       first = new X
       second = new X
   }
   **ref** first : X
   **ref** second : X
}

However, the results can be surprising:

val a : XPair // I want an independent value, please! val b = a // and a copy of that value a.first.mutate() // Oops, changes b.first!

If XPair had been declared a class, :

val a : XPair      // I want an independent value, please!

would only compile if XPair was also Clonable, thereby protecting the user's intention to create an independent value

Getting the ref out of a class instance declared val

A class instance is always accessed through a reference, but when an instance is declared val, that reference is effectively hidden behind the val wrapper. However, because this is passed to class methods as a reference, we can unwrap the underlying ref as follows:

val x : SomeClass

extension SomeClass {
  func get_ref() { return this }
}

ref y : x.get_ref()
y.mutate()          // mutates x

Teachability

By expanding the type system we have added complexity to the language. To what degree will these changes make Swift harder to learn?

We believe the costs can be mitigated by teaching plain var programming first. The need to confront val and ref can be postponed until the point where students must see them in the interfaces of library functions. All the same standard library interfaces that could be expressed before the introduction of val and ref can still be expressed without them, so this discovery can happen arbitrarily late in the game. However, it's important to realize that having val and ref available will probably change the optimal way to express the standard library APIs, and choosing where to use the new capabilities may be an interesting balancing act.

(Im)Mutability

We have looked, but so far, we don't think this proposal closes (or, for that matter, opens) the door to anything fundamentally new with respect to declared (im)mutability. The issues that arise with explicit val and ref also arise without them.

Bikeshed

There are a number of naming issues we might want to discuss. For example: