Swift Digest
SE-0507 | Swift Evolution

Borrow and Mutate Accessors

Proposal
SE-0507
Authors
Meghana Gupta, Tim Kientzle
Review Manager
Doug Gregor
Status
Implemented (Swift 6.4)

01 何が問題だったのか

computed propertyやsubscriptを実装するアクセサとして、Swiftにはこれまで get / set と、SE-0474で導入された yielding borrow / yielding mutate がありました。しかし、これらだけではうまくカバーできないケースがあります。

get アクセサは値をコピーして返すか、新しく構築して返すかのいずれかです。そのため、コピーが高価だったりコピーできなかったりする stored property をそのまま公開する用途には使えません。特に、non-copyable(~Copyable)な値を要素として持つコレクションでは、subscriptを get で書くことができません。

struct NC: ~Copyable { ... }
struct ContainerOfNoncopyable {
    private var _element: NC
    var element: Element {
        return _element // 🛑 ERROR: Cannot copy `_element`
    }
}

yielding borrow / yielding mutate は、アクセスの前後で処理を挟めるコルーチンとしてアクセサを定義できる強力な仕組みですが、そのぶん次のような欠点があります。

  • コルーチンのためのワーキングスペース確保や複数回の関数呼び出しが発生し、インライン化されない場面ではオーバーヘッドが大きくなります。
  • アクセスのスコープがコルーチンの実行スコープで閉じる必要があるため、借りた値から派生した値を呼び出し元の関数から返すことができません。たとえば、ある値の Span を取り出して返そうとすると、Span が依存している元の値のアクセスは関数が戻る前に終わってしまうため、lifetimeの制約に引っかかります。
struct Element: ~Copyable {
  var span: Span<...> { ... }
}

struct Wrapper: ~Copyable {
    private var _element: Element
    var element: Element {
        yielding borrow {
            yield _element
        }
    }
}

func getSpan(wrapper: borrowing Wrapper) -> Span<...> {
    // `element` への yielding borrow アクセスは getSpan が戻る前に終わる必要があるが、
    // `span` は `element` のlifetimeに依存しているので、
    // 🛑 ERROR: lifetime-dependent value escapes its scope
    return wrapper.element.span
}

つまり、「stored propertyをコピーせずにそのまま借して返したい」「Span のような借用ベースの値を返したい」というユースケースに対して、get はコピーを要求し、yielding borrow / yielding mutate はコルーチンのオーバーヘッドとスコープ制約を持ち込んでしまう、という隙間が残っていました。

02 どのように解決されるのか

コンテキスト依存のキーワード borrowmutate を使う、新しい非コルーチン型のアクセサを導入します。yielding borrow / yielding mutate のコルーチン版が yield で値を貸し出していたのに対し、borrow / mutate アクセサはふつうの関数のように return で stored property を返します。

struct RigidWrapper<Element: ~Copyable>: ~Copyable {
    var _element: Element
    var element: Element {
        borrow {
            return _element
        }
        mutate {
            return &_element
        }
    }
}

borrow は読み取り専用の借用として、mutate はインプレースな変更も許す借用として値を公開します。mutatereturn には & を付けます。return キーワードは、ほかの関数と同様に本体が単一の式のときは省略できます。

戻せるのは「生きている stored 値」だけ

borrow / mutate アクセサが返せるのは、アクセサの実行を超えて生き続けることが保証された stored な値だけです。ローカル変数や一時値は返せません。

struct InvalidExamples {
    var _array : [Int]

    var local: [Int] {
        borrow {
            let foo = [1, 2, 3]
            // 🛑 ERROR: Cannot return local value from borrow accessor
            return foo
        }
    }

    var temporary: [Int]? {
        borrow {
            // `_array` から一時的に `Optional` を作る必要があるのでNG
            // 🛑 ERROR: Cannot return temporary value from borrow accessor
            return _array
        }
    }
}

この制約により、たとえば _s: String? をそのまま返す borrow は書けますが、i?.s のように optional chaining で nil の場合の String?.none を生み出すような式は返せません。

呼び出し側の振る舞い

borrow で実装されたプロパティの読み出し構文は get の場合と同じですが、コピーは発生しません。借用中は exclusivity ルールにより、親の値を変更することはできません。

var owner = Wrapper(value)

doSomething(with: owner.element)

func doSomething(with value: borrowing Element) {
    value.someMethod()

    // 借用中は親のミューテーションは禁止
    owner.mutatingMethod() // 🛑 ERROR
}

mutate で実装されたプロパティは inout 引数への渡し方などで読み書き両方ができます。こちらも借用中は親に触れられません。

var owner = Wrapper(value)

doSomeMutation(with: &owner.element)

func doSomeMutation(with value: inout Element) {
    value.someMutatingMethod()

    owner.anyMethod() // 🛑 ERROR
}

他アクセサとの併用ルール

borrow / mutate を使うときは、次のルールに従う必要があります。

  • mutate を定義する場合、borrow も必ず定義します。Swiftは一般に書き込み専用プロパティを許しておらず(set のみのプロパティが禁じられているのと同じ理由)、さらに読み書きのアクセススコープを揃えることで、コンパイラが読み書き混在の操作を一つの mutate アクセスにまとめて最適化する余地を残したいためです。
  • mutate を定義する場合、yielding mutateyielding borrow は併用できません。
  • borrow を定義する場合、getyielding borrow は併用できません。

読み書きそれぞれで複数のアクセサを同時に定義することは、呼び出し側からどれが実際に使われるのか分かりにくくなるため禁止されています。ABI維持のためにコンパイラが別のアクセサを合成することはありますが、その場合はABI上の規則によりどれが使われるかが一意に決まります。

mutating / nonmutating 修飾子

既定では、borrow は親の値を変更しないアクセサ、mutate は親の値を変更するアクセサとして扱われます。mutating get / nonmutating set と同じ要領で、これを明示的に上書きできます。

mutating borrow は、呼び出し側からは読み取りにしか使えないものの、アクセサ内部で親の値に副作用が及ぶ可能性があることを表します。この場合、イミュータブルな値に対しては呼び出せません。

struct S1 {
  private var cachedValue: Foo
  var foo : Foo {
    mutating borrow {
      if !cachedValue.available {
        // `mutating` なのでここでの更新が許される
      }
      return cachedValue
    }
  }
}

let s1: S1
s1.foo // 🛑 Cannot use mutating accessor on immutable value

逆に nonmutating mutate は、アクセサが可変な借用を返すものの、その変更は親の値の変更とはみなされないことを表します。親の外部に保持されている値への参照を返すようなケースで有用です。

struct Outer {
  var inner: InnerType {
    borrow {
      return some_value_stored_elsewhere
    }
    nonmutating mutate {
      return &some_value_stored_elsewhere
    }
  }
}

プロトコル要件としての borrow / mutate

borrow / mutate はプロトコル要件としても書けます。

protocol BorrowingAccess {
  associatedtype Element
  var element: Element { borrow mutate }
}

プロトコルでアクセサを要求すると、(1)そのプロトコル越し(既存型や制約付きジェネリック引数)にプロパティをどう触れるかが決まり、(2)適合型はそのアクセサを提供する必要があります。適合型が stored property を持っていれば、コンパイラが borrow / mutate を合成できます。適合型が borrow を提供していれば、そこから yielding borrow や(copyable な場合に限り)get を合成できます。mutate を提供していれば setyielding mutate を合成できます。

プロトコルが borrow を要求する場合、適合型は borrow を明示的に提供する必要があります。mutate を要求する場合は、mutateborrow の両方を明示的に提供する必要があります。

クラスとアクターでは使えない

クラスやアクターのプロパティアクセスは、アクセスの前後両方で実行時の exclusivity チェックが必要です。borrow / mutate アクセサにはアクセス後に処理を差し挟む仕組みがないため、クラスやアクターのプロパティには使えません。コルーチン版の yielding borrow / yielding mutate は引き続きクラスのプロパティに使えます。

class ClassType {
  private var _value: SomeType
  var value: SomeType {
    borrow {
      // 🛑 Cannot use borrow to implement a property of a class or actor type
      return _value
    }
  }
}

subscriptでの利用

subscriptにも同じように書けます。

struct ArrayLikeType {
  subscript(index: Int) -> Element {
    borrow { .... }
    mutate { .... }
  }
}

subscriptの borrow / mutate アクセスは、アクセス中は構造体全体をアクセス対象とみなします。そのため、同じ値への2つのミューテーティングアクセスを同時に持つような呼び出しは禁止されます。

var x: ArrayLikeType
swap(&x[0], &x[1]) // 🛑 同時に2つの mutate アクセスが必要になる

グローバル変数

グローバル varborrow / mutate することはできません。グローバル let については borrow は可能ですが、mutate はできません。

var mutableGlobal: SomeType
let constantGlobal: SomeType

struct BorrowingGlobals {
  var mutable: SomeType {
    borrow {
      // 🛑 Cannot borrow a mutable global
      return mutableGlobal
    }
    mutate {
      // 🛑 Cannot mutate a mutable global
      return &mutableGlobal
    }
  }

  var constant: SomeType {
    borrow {
      // OK
      return constantGlobal
    }
    mutate {
      // 🛑 Cannot mutate a non-mutable value
      return &constantGlobal
    }
  }
}

採用時の注意

既存の非 borrow 系アクセサと borrow / mutate の間の切り替えは、一般にABI破壊になります。existential型のABIは、コンパイラが適合アクセサを合成できる限りにおいて保たれます。

また、getborrow に変更すると、呼び出し側に対して借用中のlifetime制約が新たに課されるため、これまでコンパイルできていたコードがコンパイルできなくなる可能性があり、ソースレベルでも破壊的変更になり得ます。

今後の展望

今回は stored property を直接返す形に限定されていますが、周辺の拡張が将来の方向性として議論されています(speculativeなもので、実現を約束するものではありません)。

  • borrowing return: 関数の戻り値として借用値を返せるようにする拡張です。たとえばsubscriptの borrow アクセサで取得した値を、そのまま別のメソッドから borrowing Value として返せるようになります。
  • unsafe pointer経由の borrow: UnsafePointer<Element> のような型は、コンパイラがlifetimeを正確に追跡できないため、そのままでは borrow アクセサから返せません。unsafeResultDependsOnSelf(...) のような印を付けて、「結果の値は次の self のミューテーションまで有効である」とユーザ側から宣言できるような仕組みが検討されています。
  • 複数の return: 現在の実装では、borrow / mutate 内に return 文を一つしか書けません。将来的に条件分岐で複数の stored property のうちどれを返すかを切り替えられるようにすることが想定されています。
  • borrowing switch との組み合わせ: borrowing switch の実装上の制約が解消されれば、enum の各ケースで異なる値を借用して返すような書き方もサポートされる見込みです。
  • ローカルな computed property: 関数内で borrow / mutate を使ったcomputed propertyを定義できるようにする方向も検討されています。
  • クラスの let プロパティ: 将来的には、クラスの let プロパティに対して borrow アクセサを提供できるようにすることが想定されています。