Enhanced 'do' Block Syntax and Error Handling

Introduction

This proposal aims to enhance the 'do' block syntax and error handling in Swift, providing more expressive code and improving the clarity and flexibility of error handling. It suggests three main changes to the language:

  1. Allowing Throwing Calls Without do Blocks: Allow calling throwing functions or methods without requiring a surrounding 'do' block, while still preserving the need for a 'catch' block to handle any potential errors.

  2. Making do Blocks Expressions: Treat do blocks as expressions, enabling more flexible usage such as assigning the result of the block to a variable or passing it as an argument to another function.

  3. Supporting Extended Syntax for do Blocks: Introduce extended syntax for 'do' blocks to convey additional semantics, enabling developers to express inline scoped initialization, mutation, and consumption operations on values.

Motivation

The proposed changes aim to simplify and streamline Swift code, making it more expressive and readable. By allowing throwing functions to be called without do blocks we can expect a wider use of error handling done with throwing syntax. By treating do blocks as expressions, Swift code can be more concise and easier to write, as they are much more natural choice comparing to IIFs. Additionally, the extended syntax for do blocks allows developers to express two very common patterns: (1) local scoped modification of a value (well known as with function) and (2) writting extensions for types to be used in only one place in code.

Proposed Changes in Detail

  1. Allowing Throwing Calls Without do Blocks: This change will remove the need for redundant do blocks when calling throwing functions or methods. Developers can handle errors with a corresponding 'catch' block, improving code clarity.

    func fetchData() throws -> Data {
      // ...
    }
    
    let data = try fetchData() catch {
      // Handle the error here by enriching it and rethrow
    }
    
  2. Making do Blocks Expressions: By treating 'do' blocks as expressions, developers can use them in more contexts, including variable assignments and function arguments.

    let result = do {
        let randomNumber = Int.random(in: 1...10) // randomNumber doesn't pollute the outer scope
        let square = randomNumber * randomNumber
        square // The last expression is the result of the 'do' block
    }
    
  3. Supporting Extended Syntax for do Blocks: The proposed extended syntax provides additional syntax for do keyword blocks, enabling developers to express initialization, mutation, and consumption operations on values inline at the call site rather than defining them as extensions. do blocks seems to be the best choice to define a scope in which a piece of code will be running. Competing approach could be an analog to Kotlin's scoped functions, but I find them confusing. Scoped functions hide reason why the type of this was changed. Another big difference between do {...} expression and .apply { ... } function with a passed closure is forced Exactly-Once semantic, meaning that the code inside the block is known to be executed once, not relying on implementation of the apply function.
    List of extensions of the do block includes:

    • do init Type { ... }: Denotes initialization semantics for a type using a do block syntax.
      struct Point {
          var x: Int = 0
          var y: Int = 0
          init() {}
      }
      
      let xStr: String = "10"
      let yStr: String = "20"
      
      let p = do init Point {
          self.init()
          x = Int(xStr)!
          y = Int(yStr)!
      }
      
    • do with value { ... }: Indicates operating on a value, similar to extensions, where the do block's scope is bound to the value via self and the result type is inferred from.
      let separator: String = ","
      
      let pointStr: String = do with Point() {
          String(self.x) + separator + String(self.y)
      }
      
    • do mutating value { ... }: Specifies that the do block mutates the value.
      var point = Point()
      
      let wasSwapped = do mutating point {
          if self.x == self.y {
              false
          }
          (self.x, self.y) = (self.y, self.x)
          true
      }
      
    • do consuming value { ... }: Signifies that the do block consumes the value.
      let point = Point()
      
      let separator: String = ","
      let pointStr: String = do consuming point {
          String(self.x) + separator + String(self.y)
      }
      // point variable is unavailable here because it was consumed, and its lifetime ended
      
    • do modifying value { ... }: Shows that the do block modifies the value and guarantees to return self.
      let xStr: String = "10"
      let yStr: String = "20"
      
      let p = do modifying Point() {
          x = Int(xStr)!
          y = Int(yStr)!
      }
      

    Notes on naming
    I tried to stick to already known Swift keywords in these new kinds of do block syntax, but there are two exceptions:

    • with is equivalent to func f() without modifiers. I admit it's a bad choice and will be waiting for your suggestions.
    • modifying could be expressed via mutating of course, but I find this pattern quite popular to consider introduction of a new keyword.

    All the other keywords are used in exactly same meaning as they used in other places in the language.

Please share your thoughts and feedback on this proposal.

Thank you for considering this proposal.

8 Likes
  • +1
  • -1
0 voters

I find this interesting, given a previous attempt at guaranteeing unique closure execution has failed.

Is this valid? Int only has failable initialisers for constructing from a String, yet your Point's member variables aren't optional.

Also, can you elaborate as to how this do init … syntax is better than the current way of doing this:

let p = Point(x: Int(xStr)!, y: Int(yStr)!)
3 Likes

Can you clarify what this is intended to do? It looks like it's creating a new Point instance with default values (x & y are 0), then constructing a string containing those. What's the advantage over the existing way to do that:

let point = Point()
let pointStr = String(point.x) + separator + String(point.y)
1 Like

What's the benefit of that over the existing way of doing this:

let point = Point()

let separator: String = ","
let pointStr: String = String(self.x) + separator + String(self.y)
_ = consume point

Or similarly:

do {
    let point = Point()
    // Use `point`.
} // `point` goes away.
1 Like

Nice catch, fixed, thanks.

This addresses situations when the provided initializer of the type doesn't allow us to specify all the properties we want. I've fixed Point type in my example to illustrate this, but you might find more real world examples in this thread.

That's exactly what it does. do with _expr_ takes value of _expr_ and evaluates the body as if it an extension function called on the value. In other words it's almost equivalent to:

extension Point {
  func makeString(separatedBy separator: String) -> String {
    x + separator + y
  }
}

let separator: String = ","
let pointStr = Point().makeString(separatedBy: separator)

The differences is that the extension function requires all used variables from the outer scope to be passed as arguments. do-block can just use them as they are directly visible.

In case when transformation of Point to something else can be a single line expression it doesn't really matter, but this isn't always the case. Sometimes we might need more complex code with multiple statements.

let vector = Vector3D(x: 1.0, y: 2.0, z: 3.0)

let vectorInfo = do with vector {
    let xSquare = self.x * self.x
    let ySquare = self.y * self.y
    let zSquare = self.z * self.z
    
    let sumOfSquares = xSquare + ySquare + zSquare
    let magnitude = sqrt(sumOfSquares)
    
    print("Vector: (\(self.x), \(self.y), \(self.z))")
    print("Magnitude: \(magnitude)")
    
    // Return any value you want from the `do with` block
    (sumOfSquares, magnitude)
}

Note that this way:

  • we don't pollute the outer scope with variables that we won't need.
  • unneeded variables will be released upon leaving the scope, so we shorten their lifetime.
  • we don't introduce an extension that will be used once.
  • we still provide a clear visual hint that this code does something coherent.

I'm not quite sure what you meant by this example. If you meant this

let pointStr: String
do {
  let point = Point()
  pointStr = ...
}
// use pointStr

Then the benefits would be:

  • we are getting type inference to our disposal
  • we can omit some uses of variable names

If you meant something more nested, like this:

do {
  let point = Point()
  let pointStr = ...
  // use pointStr
}

Than the benefits would be:

  • we keep the "happy path" low-indented.
  • in case we are dealing with some throwable code inside the block, it's nice to have catch block as close as possible.

Please also note, that the pitch doesn't declare it introduces something to the language that is impossible now. It's all about convenience and ergonomics.

Right. I think that's what's going to be most challenging: demonstrating that the additional language complexity and taller learning curve is worth the convenience.

As a meta-idea: perhaps you should add a "0" option to your poll? I'm not yet seeing anything here that's compelling to me, but then different folks can have different experiences and needs from the same language. So while I don't (yet) see a reason to +1 it, I also don't necessarily want to -1 it. I could just abstain, of course, but I think that - especially for syntactic sugars - it's valuable to see how many people are indifferent.

As you've probably noticed, for me at least it's mainly the third proposed change that's raising questions, and the second a little bit. The first seems like a good idea to me (though I'm pretty sure it's been pitched multiple times before, which leads to the question of why it was rejected in the past, and what's changed?). There doesn't seem to be a strong connection, let-alone dependency, between these three proposed changes. Perhaps they should each be pitched separately?

But back to the pitch at hand, a few more questions:

  • For do as an expression, what's the benefit over the existing syntax of let result = { … }()?

    • What about an alternative 'sugar' where if the variable is explicitly typed with something that's not a closure, the 'closure' is called automatically (and type inference goes in knowing the return value's type, saving it time? Then you don't need the do keyword at all. e.g.:

      let result: Int = {
          let randomNumber = Int.random(in: 1...10)
          randomNumber * randomNumber
      }
      

      By the same token:

      let components: URLComponents = init {
          $0.host = "example.com"
          $0.path = "xyz"
      }
      
  • Your revised do init … example feels a bit contrived - why have a Point struct with a dud initialiser like that? Feels like that Point struct is badly designed, and should just be fixed.

    The more realistic example often cited in the with pitch thread is based around URLComponents. Are there others?

    In all these examples (so far), the obvious question is: why isn't there a formal initialiser that better suits the needs of the users? Is that not the real problem?

    • Broadly-speaking, your examples will be more compelling if they use common, real-world pain-points, whether that be URLComponents or whatever. Even if that makes your examples a little more complicated.
  • Using the keyword self for the captured object seems ripe for confusion due to the (very likely) shadowing. Is there a particular reason to use that, rather than e.g. $0 as is the current convention for implicitly-named closure arguments?

    • Can the closure explicitly name the argument if it chooses, with the usual { foo in … } syntax?
  • Can all these do expressions have a catch clause?

    • How exactly would that work, e.g. in the closure-like cases does it have access to the captured object? Does it see any modifications made to it, or does it see it in the original form?

    • In the simplest do { … } case can the catch block do anything other than [re]throw or return an object of the same type as the do body?

  • Presumably if there's not a catch clause and the do body may throw, it'd have to be called with let x = try do …? I ask because while not obviously or necessarily a problem, that would be novel to the do statement, so it bears at least pointing out clearly.

  • From an English language perspective, the do in do xyz … seems to be redundant, or even add a bit of grammatical awkwardness. A simple with value { … } or mutating value { … } seems sufficient for readability. Is the do necessary?

  • mutating vs modifying seems likely to cause confusion, given they're synonyms. Is there a way to eliminate one, or combine them?

2 Likes

The idea of do init in this proposal sounds similar to the with function requested in another pitch. I'd love to have something like this. The ability to construct an object within the current context guaranteeing its existence after the block is over and make it a const afterwards is something I would have used a lot in a past few days.

One thing that severely frustrating to me is when the constants I'd like to construct are based on looping through data. It sometimes feels wasteful to use functional programming tropes like filter to iterate through a list and extract two booleans out of it, something like this:

    var isCustom = false
    var hasSpecialCase = false
    for object in array {
        if object.isCustom {
            isCustom = true
        } else if object.hasSpecialCase {
            hasSpecialCase = true
        }
    }

The example looks a bit contrived but I had some legitimate usages. I'd love it if the two vars were lets instead. I guess you could also use a block to do that but if you also want to break out of a loop you can't do that from the block.

I tried but the forum rejected the change with an error message: An error occurred: You cannot change a poll after the first 5 minutes.

Yes, you are right they should be pitched separately. But as a starting point I think it's better to provide an all-in-one vision of how all the changes will affect the language.
Another reason for them to be pitched together is that some connection between them exist. The third change depends on the second one. And the first one is intended to break a strong relation between do blocks and error handling.
In my opinion do block is an ultimate alternative to IIF(immediately invoked function). And using them almost exclusively with error handling seems to be a waste of a valuable concept.
That's why I designed the whole third change around do blocks. And that's why I want to segregate error handling from do blocks (but without breaking changes).

  1. Scoped rebinding of self
  2. It doesn't work great with move-only types.

I can't disagree that this approach is an alternative. But to me it feels more confusing with regular closures. The beauty of do is that it stand for "do now" without other meanings.

This is too much like a trailing closure.

It's done intentionally. We are not always in control of types we use. URLComponents is an example, but you might say the same about a lot of types from SwiftUI for example.
Maybe I should have choose more illustrative examples, but I tried them to be as short as possible for readers convenience.

Yes, the inner self shadows the outer. But I want the block's code to have the same name resolution properties as is function defined on a type via extension.
Perhaps we can consider to extend the syntax to allow an arbitrary name within the block. For example:

do with Point() as point {
  // `point` refers to an instance of Point
  // `self` refers to the outer `self`
}

I intend these changes to be extension to the existing do-catch syntax, so do expressions should support catch clause. I think the captured value shouldn't be accessible within catch block, but it's debatable.

  func throwingFunc() throws -> Int {}

  let result = do init Point {
    self.init()
    x = try throwingFunc()
  } catch let error {
    // Instance of `Point` is inaccessible here.
    // This block should provide a fallback for `result`
    // or interupt execution of the function via
    // 1. rethrow
    // 2. return
  }

In case we need to handle an error with the scoped value we should use a nested catch clause:

  let result = do init Point {
    self.init()
    x = try throwingFunc() catch let error {
      // Instance of `Point` is accessible here.
      // This clause should provide a fallback for `x`
      // or interupt execution of the function via
      // 1. rethrow
      // 2. return
    }
  }

When the do-block is a throwing block and the function is marked with throws keyword, the catch clause is optional. If it's provided it will be responsible for handling errors, otherwise errors will be propagated to the outside.

func throwingFunc() throws -> Int { ... }

func foo() throws -> Int {
  let point = do with Point() {
    x = try throwingFunc()
  }
  ...
}

func bar() -> Int {
  let point = do with Point() {
    x = try throwingFunc()
  } catch let error {
    // ...
  }
  ...
}

As a non-native English speaker, I don't feel comfortable defending one or the other point of view. But to me having a verb in front is preferable, especially if it's a well-known language keyword. Without it I feel a bit of confusion about "? with variable { some code }". I imaging a newcomer might ask "what is happening here?". It could be a partial application of a closure for example.

I don't like these duality as well, but I also don't like the need of explicit last expression to indicate the result in mutating when it matches self.

let p = do mutating Point() {
  x = 10
  y = 20
  self // awkward
}

Ideas?

Thanks for your detailed response. A lot of it makes sense, e.g. exception handling aspects.

Can you clarify what you see the benefit being? Perhaps it shows up in more complex cases, but in these simple examples I don't see why rebinding (a.k.a. shadowing) is beneficial.

Generally I see shadowing of self to be bad, based on my experience. Shadowing in general - outside of some well-understood, blessed cases, like if let self { … } - is bad because it creates ambiguities and the possibility of errors as a result.

A variation inspired by Python is (where Python would say with Point() as p: …):

let p = with Point() { p in
    p.x = Int(xStr)!
    p.y = Int(yStr)!
}

p is repeated in this example, but this with <object> { … } syntax can be used inlined in e.g. function arguments so there's no way to avoid either explicitly naming the argument to the closure or using the standard implicit name ($0).

Well, other than using self, but as noted I think that's more dangerous. And every time I see self used this way in these examples it makes the closure look a lot like an actual initialiser, which makes me wonder why the closure shouldn't be a proper initialiser (the ability to 'inline' the initialiser is not necessarily a good thing in my mind, as it can become… complicit in code duplication and poor factoring).

I know a little about move-only types, but perhaps not enough - can you elaborate on why it doesn't work well with them?

Indeed it is syntactically ambiguous. Though one could argue that the ambiguity is beneficial, as it lets a type override the default behaviour by providing its own init that takes a closure. e.g. if it wants to use result builder syntax instead. (one could also argue that giving type definers that option is a negative)

Is SwiftUI a relevant example? It uses a combination of initialisers, result builders, and what it calls "view modifiers" - as well as strong encouragement for abstraction into named View types - to do object configuration. If this do init … { syntax had been available at its conception, would the end design have actually been any different? I haven't thought long about it, but I don't immediately see any reason SwiftUI would be better with them.

Manual use of UIKit might be a better example? I don't use it myself, but I see a lot of chatter about that practice, within the Swift community.

And I continue to maintain that URLComponents is just badly designed. I can see that for complicated objects it might be considered unwieldy to have an initialiser that covers all their properties - and maybe that's an area where more compelling examples could be found - but URLComponents is not that complicated.

Yeah, this aspect is difficult because it's subjective and human languages vary so much. There might not be an objectively correct choice, just a "popular" one.

Maybe part of it is just that "with x, do y" is more natural to me than "do with x y". The latter is like some kind of reverse-Polish version of English. :smile:

It's also like if every functional call had to be prefixed with "call" (or "do", even). e.g. let x = do computePi(). There are indeed other programming languages which have required that sort of syntax, but it seems unnecessary here (and a bit archaic). In particular, Swift uses parenthesis (or a trailing closure) to distinguish between calling an object and referencing the object. The parenthesis (or {) serve double-duty in this regard, in addition to being how function arguments are presented.

To date do has been used to distinguish a statement from a definition of a closure. In that sense it actually is kind of like an alternative to parenthesis (although, there might be important implementation differences between an immediately-called anonymous closure and a nested block, even beyond just the ability to tack catch on the end). I'm not sure what that tells us, just making the observation.

Agreed.

"modifying" vs "modified" technically works (as in "by modifying x…" vs "a modified copy of x…"), but is a subtle distinction that might confuse people, at least at first.

+1, -1

I’m a fan of continuing down the blocks return values path, but I don’t care for the do notation extension.

1 Like

I like the try foo() catch { ... } and do-expressions, but the do with/do mutating/etc are a bit confusing and superfluous to me. Dynamically scoped this is one of the most infamous and confusing parts of JavaScript, and I'd rather Swift self keep its current meaning and not be scoped. If we drop the scoped self, I'm fairly sure e.g. do consuming can be expressed as:

func doConsuming<T: ~Copyable, Result>(_ x: consuming T, body: (consuming T) throws -> Result) rethrows -> Result {
  return try body(consume x)
}

(Though I'm not sure whether T: ~Copyable is actually legal yet, my understanding is that this is at least a planned feature.)

In the same vein as do expressions, I might like to see Rust-style loop expressions:

let x = repeat {
  doSomething()
  if condition {
    break someValue
  }
}
1 Like

The one odd thing about making do blocks expressions is that the catch clauses also become expressions. I think I'm ok with this, but it is a little stranger than other blocks.

struct Foo {
  var bar: String = do {
     try fetchString()
  } catch {
     print("Today is not your day.")
     ""
  }
}
1 Like

struct NC: ~Copyable {
  var x: Int

  consuming func foo() -> Int {
    x
  }
}

func testA() {
  let nc = NC(x: 1) // OK
  let y = nc.foo() + 1
  print(y)
}

func testB() {
  let nc = NC(x: 1) // Noncopyable 'nc' cannot be consumed when captured by an escaping closure
  let y = {
    nc.foo() + 1
  }()
  print(y)
}

func testC() {
  var nc = NC(x: 1) // Error: Missing reinitialization of closure capture 'nc' after consume
  let y = {
    nc.foo() + 1
  }()
  print(y)
}

func testD() {
  var nc = NC(x: 1) // OK
  let y = { (nc: consuming NC) in
    nc.foo() + 1
  }(consume nc)
  print(y)
}

Ah, I see - thanks, that's helpful and clear.

I was assuming move-only types would be implicitly moved into a closure in cases like this. It sounds like they can be moved, but only explicitly? That does indeed add some boilerplate to that use.

Tangentially, in testC, though, why is there a problem? nc isn't used outside nor beyond the closure, so why would (or should?) the compiler complain about it not being reinitialised? I'm assuming from context that the compiler does implicitly move it into the closure when it's a var, as opposed to a let?

I wish I knew.

That's right. And I find it a little more convenient. Now you write either

let x = try? foo() ?? "fallback" 

and you can't do anything with the error, or

let x: String
do {
  x = try foo()
} catch {
  print(String(describing: error))
  x = "fallback"
}

and this isn't pretty, because of the loss of type inference.
But with the proposed change you could write

let x = try foo() catch {
  print(String(describing: error))
  "fallback"
}

I understand the confusion. But I'd like to point that Swift allows self shadowin, but only for explicit self:

class Bar {
  let x = 42
}

class Foo {
  let y = 24

  func foo() {
    let z = { [self = Bar()] in
      self.x // Ok
      x // Cannot find 'x' in scope
      self.y // Value of type 'Bar' has no member 'y'
      y // Ok
    }()
  }
}
1 Like

Ok, I'm warming up to the idea of try/catch as an expression style option since it helps preserve type inference as shown by @dmt.

If the proposed try/catch syntax were used, it might be useful for rethrowing or erasing/embedding upstream errors.

var bar: String = do {
   try fetchString() catch { throw InnerError.fetchOperationFailed }
} catch { // catch in the outer do
   print("Today is not your day.")
   ""
}

I assume both of these would work assuming guard expressions are allowed in a future version of Swift.

var bar: String = do {
  guard let result = try fetchString() catch { 
    throw InnerError.fetchOperationFailed 
  } else { 
    throw InnerError.noResult 
  }
} catch { // catch in the outer do
  print("Today is not your day.")
  ""
 }
var bar: String = do {
  let result = try guard fetchString() != nil else { 
    throw InnerError.noResult
  } catch { 
    throw InnerError.fetchOperationFailed 
  }
} catch { // catch in the outer do
  print("Today is not your day.")
  ""
 }

just a useful tip i discovered recently: you can also shadow self by making it a parameter of a static operator func. i find this makes it a lot easier to factor mutating methods into inout operators like +=.

1 Like