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
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. -
Making
do
Blocks Expressions: Treatdo
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. -
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
-
Allowing Throwing Calls Without
do
Blocks: This change will remove the need for redundantdo
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 }
-
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 }
-
Supporting Extended Syntax for
do
Blocks: The proposed extended syntax provides additional syntax fordo
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 ofthis
was 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 theapply
function.
List of extensions of thedo
block includes:do init Type { ... }
: Denotes initialization semantics for a type using ado
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 thedo
block's scope is bound to the value viaself
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 thedo
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 thedo
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 thedo
block 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 ofdo
block syntax, but there are two exceptions:with
is equivalent tofunc f()
without modifiers. I admit it's a bad choice and will be waiting for your suggestions.modifying
could be expressed viamutating
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.