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:
-
Allowing Throwing Calls Without
doBlocks: 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. -
Making
doBlocks Expressions: Treatdoblocks 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. -
Supporting Extended Syntax for
doBlocks: 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
-
Allowing Throwing Calls Without
doBlocks: This change will remove the need for redundantdoblocks 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 } -
Making
doBlocks 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 } -
Supporting Extended Syntax for
doBlocks: The proposed extended syntax provides additional syntax fordokeyword blocks, enabling developers to express initialization, mutation, and consumption operations on values inline at the call site rather than defining them as extensions.doblocks 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 ofthiswas changed. Another big difference betweendo {...}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 theapplyfunction.
List of extensions of thedoblock includes:do init Type { ... }: Denotes initialization semantics for a type using adoblock 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 thedoblock's scope is bound to the value viaselfand 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 thedoblock 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 thedoblock 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 endeddo modifying value { ... }: Shows that thedoblock modifies the value and guarantees to returnself.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 ofdoblock syntax, but there are two exceptions:withis equivalent tofunc f()without modifiers. I admit it's a bad choice and will be waiting for your suggestions.modifyingcould be expressed viamutatingof 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.