Un-pyramid-of-doom the `with` style methods

withXYZ { ... }-style methods give you scoped access to a resource. There are a couple of reasons why you might need that, and there are already some sketches of ways we could improve them. They rely on transforming these methods in to yield-once coroutines.

So instead of:

func withXYZ<Result>(
  _ body: (SomeResource) throws -> Result
) rethrows -> Result {

  // setup.
  let resource = SomeResource(...)
  // cleanup.
  defer { ... }
  // body.
  return try body(resource)
}

// Caller requires closure scope:

someValue.withXYZ { resource in
  resource.doSomething()
}

You have:

var xyz: SomeResource {
  read {
    // setup.
    let resource = SomeResource(...)
    // cleanup.
    defer { ... }
    // body.
    yield resource
  }
}

// Caller does not need to write a closure scope:

someValue.xyz.doSomething()

That's the basic idea. Of course, to fully tackle scoped resources, it needs to do more:

  1. read/modify/yield do not yet officially exist.

  2. The yield finishes when the property access completes. We need the ability to extend the access, so we can perform multiple operations on the resource without invoking the coroutine multiple times or introducing a closure scope. Basically, we need to bind it to a name, do some work on it, then drop it to end the access.

  3. read/modify (as currently implemented) only apply to properties. Sometimes you need parameters to configure a scoped resource, so it would be cool if methods could also become yield-once coroutines.

  4. Closure scopes are sometimes used as a visual indication that the provided resource has a limited lifetime. Yielded values would lack that.

Happily, there are ideas which could solve all of these issues. For #2, we might introduce borrow variables:

For #3, my hope is that we extend the idea of ref/inout variables to function return types.

For example, I'm currently working on an API for WebURL which provides a view of a URL component as a list of key-value pairs. The view can be configured with a URL component and schema, but this unfortunately means it can't take advantage of modify accessors (properties don't support parameters). This API is only possible using a closure scope:

// Example: working with W3C media fragments:

var url = WebURL("http://www.example.com/example.ogv#track=french&t=10,20")!

url.withMutableKeyValuePairs(in: .fragment, schema: .percentEncoded) { kvps in
   kvps["track"] = "german"
}

print(url)
// âś… "http://www.example.com/example.ogv#track=german&t=10,20"
//                                              ^^^^^^

I hope users will eventually be able to write something like:

var url = WebURL("http://www.example.com/example.ogv#track=french&t=10,20")!

inout kvps = url.keyValuePairs(in: .fragment, schema: .percentEncoded)
kvps["track"] = "german"
// <drop 'kvps' somehow to end the access to 'url'>

print(url)
// âś… "http://www.example.com/example.ogv#track=german&t=10,20"
//                                              ^^^^^^

Or even one-liners like:

url.keyValuePairs(in: .fragment, schema: .percentEncoded)["track"] = "german"

To tackle #4, there are sketches for @nonescaping read and modify coroutines, which should help express the idea that the yielded value has a limited lifetime.

So the issue has definitely been noticed. Besides making code more difficult to read, closure scopes are just not enough for important things like enforcing lifetimes. As we gain these new expressive capabilities, we'll hopefully be able to replace uses of the withXYZ { ... } pattern.

12 Likes