Back to Swift

Immutability and Read-Only Methods

docs/MutationModel.rst

latest9.5 KB
Original Source

:orphan:

.. raw:: html

<style>
table.docutils td,
table.docutils th {
  border: 1px solid #aaa;
}
</style>

================================== Immutability and Read-Only Methods

:Abstract: Swift programmers can already express the concept of read-only properties and subscripts, and can express their intention to write on a function parameter. However, the model is incomplete, which currently leads to the compiler to accept (and silently drop) mutations made by methods of these read-only entities. This proposal completes the model, and additionally allows the user to declare truly immutable data.

The Problem

Consider::

class Window {

var title: String { // title is not writable
  get {
    return somethingComputed()
  }
}

}

var w = Window() w.title += " (parenthesized remark)"

What do we do with this? Since += has an inout first argument, we detect this situation statically (hopefully one day we'll have a better error message):

.. code-block:: swift-console

<REPL Input>:1:9: error: expression does not type-check w.title += " (parenthesized remark)"

^~~~~~~~~~~~~~~~~~~~~~~~~~~~

Great.  Now what about this? [#append]_ ::

 w.title.append(" (fool the compiler)")

Today, we allow it, but since there's no way to implement the
write-back onto ``w.title``, the changes are silently dropped.

Unsatisfying Approaches
=======================

We considered three alternatives to the current proposal, none of
which were considered satisfactory:

1. Ban method calls on read-only properties of value type
2. Ban read-only properties of value type
3. Status quo: silently drop the effects of some method calls

For rationales explaining why these approaches were rejected, please
refer to earlier versions of this document.

Proposed Solution
=================

Terminology
-----------

Classes and generic parameters that conform to a protocol attributed
``@class_protocol`` are called **reference types**.  All other types
are **value types**.

Mutating and Read-Only Methods
------------------------------

A method attributed with ``inout`` is considered **mutating**.
Otherwise, it is considered **read-only**.

.. parsed-literal::

  struct Number {
    init(x: Int) { name = x.toString() }

    func getValue() {              // read-only method
      return Int(name)
    }
    **mutating** func increment() {  // mutating method
      name = (Int(name)+1).toString()
    }
    var name: String
  }

The implicit ``self`` parameter of a struct or enum method is semantically an
``inout`` parameter if and only if the method is attributed with
``mutating``.  Read-only methods do not "write back" onto their target
objects.

A program that applies the ``mutating`` to a method of a
class--or of a protocol attributed with ``@class_protocol``--is
ill-formed.  [Note: it is logically consistent to think of all methods
of classes as read-only, even though they may in fact modify instance
variables, because they never "write back" onto the source reference.]

Mutating Operations
-------------------

The following are considered **mutating operations** on an lvalue

1. Assignment to the lvalue
2. Taking its address

Remember that the following operations all take an lvalue's address
implicitly:

* passing it to a mutating method::

   var x = Number(42)
   x.increment()         // mutating operation

* passing it to a function attributed with ``@assignment``::

   var y = 31
   y += 3                // mutating operation

* assigning to a subscript or property (including an instance
 variable) of a value type::

   x._i = 3             // mutating operation
   var z: Array<Int> = [1000]
   z[0] = 2             // mutating operation

Binding for Rvalues
-------------------

Just as ``var`` declares a name for an lvalue, ``let`` now gives a
name to an rvalue:

.. parsed-literal::

  var clay = 42
  **let** stone = clay + 100 // stone can now be used as an rvalue

The grammar rules for ``let`` are identical to those for ``var``.

Properties and Subscripts
-------------------------

A subscript or property access expression is an rvalue if

* the property or subscript has no ``set`` clause
* the target of the property or subscript expression is an rvalue of
 value type

For example, consider this extension to our ``Number`` struct:

.. parsed-literal::

 extension Number {
   var readOnlyValue: Int { return getValue()  }

   var writableValue: Int {
     get {
      return getValue()
     }
     **set(x)** {
       name = x.toString()
     }
   }

   subscript(n: Int) -> String { return name }
   subscript(n: String) -> Int {
     get {
       return 42
     }
     **set(x)** {
       name = x.toString()
     }
   }
 }

Also imagine we have a class called ``CNumber`` defined exactly the
same way as ``Number`` (except that it's a class).  Then, the
following table holds:

+----------------------+----------------------------------+------------------------+
|          Declaration:|::                                |                        |
|                      |                                  |::                      |
|Expression            |   var x = Number(42)  // this    |                        |
|                      |   var x = CNumber(42) // or this |  let x = Number(42)    |
|                      |   let x = CNumber(42) // or this |                        |
+======================+==================================+========================+
| ``x.readOnlyValue``  |**rvalue** (no ``set`` clause)    |**rvalue** (target is an|
|                      |                                  |rvalue of value type)   |
|                      |                                  |                        |
+----------------------+                                  |                        |
| ``x[3]``             |                                  |                        |
|                      |                                  |                        |
|                      |                                  |                        |
+----------------------+----------------------------------+                        |
| ``x.writeableValue`` |**lvalue** (has ``set`` clause)   |                        |
|                      |                                  |                        |
+----------------------+                                  |                        |
| ``x["tree"]``        |                                  |                        |
|                      |                                  |                        |
+----------------------+----------------------------------+                        |
| ``x.name``           |**lvalue** (instance variables    |                        |
|                      |implicitly have a ``set``         |                        |
|                      |clause)                           |                        |
+----------------------+----------------------------------+------------------------+

The Big Rule
-------------

.. Error:: A program that applies a mutating operation to an rvalue is ill-formed
  :class: warning

For example:

.. parsed-literal::

  clay = 43           // OK; a var is always assignable
  **stone =** clay \* 1000 // **Error:** stone is an rvalue

  swap(&clay, **&stone**) // **Error:** 'stone' is an rvalue; can't take its address

  **stone +=** 3          // **Error:** += is declared inout, @assignment and thus
                      // implicitly takes the address of 'stone'

  **let** x = Number(42)  // x is an rvalue
  x.getValue()        // ok, read-only method
  x.increment()       // **Error:** calling mutating method on rvalue
  x.readOnlyValue     // ok, read-only property
  x.writableValue     // ok, there's no assignment to writableValue
  x.writableValue++   // **Error:** assigning into a property of an immutable value

Non-``inout`` Function Parameters are RValues
----------------------------------------------

A function that performs a mutating operation on a parameter is
ill-formed unless that parameter was marked with ``inout``.  A
method that performs a mutating operation on ``self`` is ill-formed
unless the method is attributed with ``mutating``:

.. parsed-literal::

 func f(_ x: Int, y: inout Int) {
   y = x         // ok, y is an inout parameter
   x = y         // **Error:** function parameter 'x' is immutable
 }

Protocols and Constraints
-------------------------

When a protocol declares a property or ``subscript`` requirement, a
``{ get }`` or ``{ get set }`` clause is always required.

.. parsed-literal::

  protocol Bitset {
    var count: Int { **get** }
    var intValue: Int { **get set** }
    subscript(bitIndex: Int) -> Bool { **get set** }
  }

Where a ``{ get set }`` clause appears, the corresponding expression
on a type that conforms to the protocol must be an lvalue or the
program is ill-formed:

.. parsed-literal::

 struct BS {
   var count: Int    // ok; an lvalue or an rvalue is fine

   var intValue : Int {
     get {
       return 3
     }
     set {             // ok, lvalue required and has a set clause
       ignore(value)
     }
   }

   subscript(i: Int) -> Bool {
     return true   // **Error:** needs a 'set' clause to yield an lvalue
   }
 }

-----------------

.. [#append] String will acquire an ``append(other: String)`` method as part of the
            formatting plan, but this scenario applies equally to any
            method of a value type