How can I borrow with conditional binding?

struct Foo: ~Copyable {
    var baz: Int = 0
}

final class Bar {
    var foo: Foo? = Foo()

    func update() {
        if var foo {
            foo.baz = 3
        }
    }
}

For this code I get an error: Cannot consume noncopyable stored property 'self.foo' of a class

But I think what I'm trying to do is actually to:

  • borrow
  • check for nil
  • make sure that syntactically it becomes non-optional in this scope

So, how can I perform conditional binding without consuming?
I would really like to avoid writing something like:

if foo != nil {
    foo!.baz = 3
}

I like to use take() in this situation (probably one of my favorite methods in stdlib):

struct Foo: ~Copyable {
  var baz: Int = 0
}

final class Bar {
  var foo: Foo? = Foo()
  func update() {
    if var foo = foo.take() {
      // mutate the wrapped value while uniquely referencing.
      foo.baz = 3
      // move back into self.foo.
      self.foo = consume foo
    }
  }
}
2 Likes

Not good enough because requires me to reinitialize self.foo after take()

Do you mean the line self.foo = consume foo you don't want to have to write? The if var foo {..} would require a consume, but if foo is borrowed, which is what you're saying you want to do, it can't be consumed. The error you get if you write it like this makes more sense to me:

func update(_ foo: borrowing Foo?) { // 'foo' is borrowed and cannot be consumed
  if var foo {}
}

It should be possible to allow us to write if let foo {} since the let only needs to borrow the wrapped value. As for mutation I'm not sure how that would work (not saying it can't).

1 Like

I guess, the answer to this topic - I can't.

So, what I was looking for, I just googled, is actually called ref keyword in Rust... Is there an analog in Swift?

1 Like

This would be really nice I agree, I think the closest we can get right now though is:

// foo is borrowed here.
switch foo {
  case let .some(foo):
    print(foo.baz)
  case .none:
    break
 }

For some reason the if let syntax sugar doesn't work for pattern matching but switching does. If you want to mutate it, you can use optional chaining:

func update() {
  foo?.baz += 1
}
3 Likes

This will likely require the "ReferenceBindings" experimental feature, and should look something like this:

if case .some(inout x) = foo {
  x.baz = 3
}

or simply
if inout x = foo { ... }

But it doesn't work with non-copyable types right now and the feature hasn't been proposed.

In the absence of the feature, if the need to explicitly reinitialize the original variable bothers you - you could write your own with-style extension method for Optional that passes the unwrapped value as an inout binding and handles the reinitialization internally:

extension Optional where Wrapped: ~Copyable {
  mutating func withUnwrapped<R: ~Copyable, E: Error>(_ f: (inout Wrapped) throws(E) -> R) throws(E) -> R? {
    switch consume self {
    case .none:
      self = .none
      return nil
    case .some(var value):
      do {
        let result = try f(&value)
        self = .some(value)
        return result
      } catch {
        self = .some(value)
        throw error
      }
    }
  }
}

func update() {
  foo.withUnwrapped { foo in
    foo.baz = 3
  }
}
3 Likes

I'll also point out that you tried if var foo and that doesn't borrow for copyable types either. Did you really need mutation in the first place?

3 Likes

Yeah, I think you want if inout foo = &foo as proposed in Pitch: `borrow` and `inout` declaration keywords which isn't available yet. (Or I guess behind the ReferenceBindings experimental feature @dmt mentioned?)

The alternative @jameesbrown provided is your best option today:

2 Likes

FWIW, foo?.baz += 1 is OK, but the compiler crashes when you replace it with foo?.baz = 1(simple assignment):

3 Likes