Skip to content

Latest commit

 

History

History
335 lines (259 loc) · 10.4 KB

0429-partial-consumption.md

File metadata and controls

335 lines (259 loc) · 10.4 KB

Partial consumption of noncopyable values

Introduction

We propose allowing noncopyable fields in deinit-less aggregates to be consumed individually, so long as they are defined in the current module or frozen. Additionally, we propose allowing fields of such an aggregate with a deinit to be consumed individually within that deinit. This permits common patterns to be used with many noncopyable values.

Motivation

In Swift today, it can be challenging to manipulate noncopyable fields of an aggregate.

For example, consider a Pair of noncopyable values:

struct Unique : ~Copyable {...}
struct Pair : ~Copyable {
  let first: Unique
  let second: Unique
}

It is currently not straightforward to write a function that forms a new Pair with the values reversed. For example, the following code is not currently allowed:

extension Pair {
  consuming func swap() -> Pair {
    return Pair(
      first: second, // error: cannot partially consume 'self'
      second: first // error: cannot partially consume 'self'
    )
  }
}

There are various workarounds for this, but they are not ideal.

Proposed solution

We allow noncopyable aggregates without deinits to be consumed field-by-field, if they are defined in the current module or frozen. That makes swap above legal as written.

This initial proposal is deliberately minimal:

Imported aggregates can never be partially consumed, unless they are frozen.

Detailed design

We relax the requirement that a noncopyable aggregate be consumed at most once on each path. Instead we require only that each of its noncopyable fields be consumed at most once on each path. Imported aggregates (i.e. those defined in another module and marked either public or package), however, cannot be partially consumed unless they are marked @frozen.

Extending the Pair example above, the following becomes legal:

func takeUnique(_ elt: consuming Unique) {}
extension Pair {
  consuming func passUniques(_ forward: Bool) {
    if forward {
      takeUnique(first)
      takeUnique(second)
    } else {
      takeUnique(second)
      takeUnique(first)
    }
  }
}

The struct Pair has two noncopyable fields, first and second. And there are two paths through the function: the paths taken when forward is true and when it is false. On both paths, first and second are both consumed exactly once.

It's not necessary to consume every field on every path, however. For example, the following is allowed as well:

extension Pair {
  consuming func passUnique(_ front: Bool) {
    if front {
      takeUnique(first)
    } else {
      takeUnique(second)
    }
  }
}

Here, only first is consumed on the path taken when front is true and only second on that taken when front is false.

Field lifetime extension

When a field is not consumed on some path, its destruction is deferred as long as possible. Here, that looks like this:

extension Pair {
  consuming func passUnique(_ front: Bool) {
    if front {
      takeUnique(first)
      // second is destroyed
    } else {
      takeUnique(second)
      // first is destroyed
    }
  }
}

Neither first nor second can be destroyed after the if/else blocks because that would require a copy.

Explicit field consumption

Fields can also be consumed explicitly via the consume keyword. This enables overriding the extension of a field's lifetime.

Continuing the example, if it were necessary that first always be destroyed before second, the following could be written:

extension Pair {
  consuming func passUnique(_ front: Bool) {
    if front {
      takeUnique(first)
      // second is destroyed
    } else {
      _ = consume first
      takeUnique(second)
    }
  }
}

Imported aggregates

Partial consumption of a non-copyable type is always allowed when the type is defined in the module where it is consumed. If the type is defined in another module, partial consumption is only permitted if the type is marked @frozen.

The reason for this limitation is that as the module defining a type changes, the type itself may change, adding or removing fields, changing fields to computed properties, and so on. A partial consumption of the type's fields that makes sense as the type is defined by one version of the module may not make sense as the type is defined in another version. That consideration does not apply to frozen types, however, because by marking them @frozen, the module's author promises not to change their layouts.

These rules are unavoidable for libraries built with library evolution and are applied universally to avoid having language rules differ based on the build mode.

Copyable fields

It is currently legal to have multiple consuming uses of a copyable field of a noncopyable aggregate. For example:

func takeString(_ name: consuming String) {}
struct Named : ~Copyable {
  let unique: Unique
  let name: String
  consuming func use() {
    takeString(name)
    takeString(name)
    takeString(name)
    takeString(name)
    // unique is consumed
  }
}

This remains true when a value is partially consumed:

extension Named {
  consuming func unpack() {
    takeString(name)
    takeString(name)
    takeUnique(unique)
    takeString(name)
    takeString(name)
  }
}

Partial consumption within deinits

There are two related reasons to limit partial consumption to fields of types without deinits: First, the deinit of such types can't be run if it is partially consumed. Second, no proposed mechanism to indicate that the deinit should not be run has been accepted.

Neither applies when partially consuming a value within its own deinit. We propose allowing a value to be partially consumed there.

struct Pair2 : ~Copyable {
  let first: Unique
  let second: Unique

  deinit {
    takeUnique(first) // partially consumes self
    takeUnique(second) // partially consumes self
  }
}

This enables noncopyable structs to dispose of any resources they own on destruction.

Source compatibility

No effect. The proposal makes more code legal.

ABI compatibility

No effect.

Implications on adoption

This proposal makes more code legal. And the code it makes legal is code written in a style familiar to Swift developers used to working with copyable values. It alleviates some pain points associated with writing noncopyable code, easing further adoption.

Future directions

Discard

This document proposes limiting partial consumption to aggregates without deinit. In the future, another proposal could lift that restriction. The trouble with lifting it is that the deinit can no longer be run, which may be surprising. That trouble could be mitigated by requiring the value be discard'd prior to partial consumption, indicating that the deinit should not be run.

struct Box : ~Copyable {
  var unique: Unique
  deinit {...}

  consuming func unpack() -> Unique {
    discard self
    return unique
  }
}

Partial reinitialization

This document only proposes allowing the fields of an aggregate to be consumed individually. It does not allow for those fields to be reinitialized in order to return the aggregate to a legal state. In the future, though, another proposal could lift that restriction.

That would enable further code patterns--already legal with copyable values--to be written in noncopyable contexts For example:

struct Unique : ~Copyable {}
struct Pair : ~Copyable {
  var first: Unique
  var second: Unique
}

extension Pair {
  mutating func swap() {
    let tmp = first
    first = second
    second = tmp
  }
}

Partial consumption of copyable fields

This document only proposes allowing the noncopyable fields of a noncopyable aggregate to be consumed individually. In the future, the ability to explicitly consume (via the consume keyword) the copyable fields of a copyable aggregate could be added.

class C {}
func takeC(_ c: consuming C)
struct PairPlusC : ~Copyable {
  let first: Unique
  let second: Unique
  let c: C
}

func disaggregate(_ p: consuming PairPlusC) {
  takeUnique(p.first)
  takeC(consume p.c) // p.c's lifetime ends
  takeUnique(p.second)
}

That would provide the ability to specify the point at which the lifetime of a copyable field should end.

Partial consumption of copyable aggregates

This document only proposes allowing noncopyable aggregates to be partially consumed. There is a natural extension of this to copyable aggregates:

class C {}
struct CopyablePairOfCs {
  let c1: C
  let c2: C
}
func tearDownInOrder(_ p: consuming CopyablePairOfCs) {
  takeC(consume p.c2)
  takeC(consume p.c1)
}

Alternatives considered

Explicit destructuring

Instead of consuming the fields of a struct piecewise, an alternative would be to simultaneously bind every field to a variable:

let (a, b) = destructure s

Something like this might be desirable eventually, but it would be best introduced as part of support for pattern matching for structs. Even with such a feature, the behavior proposed here would remain desirable: fields of a copyable aggregate can be consumed field-by-field, so consuming fields of a noncopyable aggregate should be supported as much as possible too.

Acknowledgments

Thanks to Andrew Trick for extensive design conversations and implementation review.