Type: New Feature
Adding Method Cascades
Author(s): Erica Sadun
Review manager: TBD
Method cascades offer a method-based counterpart to functional chaining. In functional chaining, partial results pass from one step to the next. In cascades, object scope is maintained through a series of sequential calls. Both approaches support fluent interfaces, providing readable streamlined code.
Cascades currently appear in languages including Dart and Smalltalk. The following write-ups motivate and explain the inclusion of this feature in other programming languages.
Dart language feature request: method cascades
Method Cascades in Dart
Dart-like method cascading operator in Python
It helps to start a discussion of method cascading with initialization. Under Cocoa and Cocoa touch, many Apple-supplied classes won't set up an instance during the normal Swift initialization phase. Here's one example using NSTask. This snippet customizes a new instance, manually assigning a launch path, arguments, and an output pipe.
This build-then-specialize pattern extends throughout Cocoa/Cocoa Touch. This next interface-building example uses UILabel.
These examples demonstrate common issues that motivate method cascades:
Unnecessary redundancy. Similar lines follow one after another, to the point one could say "we get it already, you're setting up a task or a label".
Inappropriate visual focus Repeated symbols (task and questionLabel) actually draw attention from the set-up these lines of code are intended to perform. When visually scanning the code, a reader's attention is more naturally drawn to the repeated block than the particular programmatic details. This cognitive overload can negatively impact code inspection.
Extra verbiage The extra text goes against Swift common succinct style. Compare, for example, .whitespaceAndNewlineCharacterSet with NSCharacterSet.whitespaceAndNewlineCharacterSet.
Ungrouped code blocks When sequentially setting up many instances, Swift provides no clear way to differentiate scope between unrelated set-up groups other than inserting whitespace gaps or building custom factory functions.
Moving beyond initialization
Although the preceding examples extend initialization, method cascades are not limited to setup. When working with NSTask, applications commonly launch() the instance and then waitUntilExit(). These non-initializer calls would also benefit from cascading:
Advantages of Method Cascading
The advantages to method cascading are as follows:
Method cascades produce fluent sequential calls that match the simplicity of functional chaining
Code is streamlined, a general Swift ideal
Cascades can extend setup, which is common when working with Cocoa classes.
An indented scope provides a visual emphasis of the single task being addressed allows easier top-level initialization for global values and in Swift playgrounds.
Cascades provide a natural alternative for serial calls that do not lend themselves to functional chaining.
Cascades could be extended for optionals as an alternative to if-let binding.
I propose to introduce a with keyword followed by an instance, a variable binding, or expression and a scope with multiple expressions using that item as a default receiver. This approach transforms following sequence of statements:
to either a non-binding version:
or a binding version:
The self receiver in the braced scope corresponds to the instance or the newly bound symbol.
The refactored NSTask example
Once introduced, the NSTask example refactors to:
The result is cleaner, easier to read, and more succinct. Cascades eliminate the need to list an object over and over again or use a temporary stand-in variable to represent the object being customized.
Binding into a new scope introduces potential reference conflicts. Consider the following example:
Method cascading must address two overlap scenarios: 1. The sharedName symbol may refer to the instance property (1), the local variable (2), or the property of myInstance (3). 1. self may refer to myInstance or the instance of MyClass
Resolving reference conflicts
This proposal uses the following scoping rules:
Internal scope always wins for symbol resolution
To access a namespace outside the scope, you must prefix a symbol with _.
Here is the proposed resolution:
Potential conflict areas
Assume, for example, that the next OS extends NSTask to include a new path property. Here is cascaded code before the OS release:
This snippet compiles both before and after the OS update but its meaning would significantly change. This introduces a notable error. After the update, the assignment continues to use rule 1 (internal scope wins for symbol resolution) and assigns a potentially uninitialized value to launchPath in preference to the external symbol. Since the Swift-language would not change, this could not be addressed through migration.
As OS changes breaking code is not limited to this proposal, I'd suggest introducing a tool that marked newly-introduced symbol conflicts, which would extend beyond this scenario: "Warning: NSTask path introduced in OS X 10.12 Malibu Barbie creates potential ambiguity on line 571."
Impact on existing code
As a newly introduced feature method cascades would not affect existing code and Swift would not require migration to accommodate its inclusion. Instead, I see this method cascading as offering a positive way to refactor code bases for greater readability and simplicity.
Method cascading can be approximated in current Swift using custom operators, for example:
I dislike this implementation for several reasons:
It lacks the clarity and fluency of a no-prefix solution that automatically establishes self.
It is limited to reference types
It requires anonymous arguments that visually stack
Chaining and Optionals
Sean Heber writes
"If you used "with" with an optional, it could just skip the entire block if the value was nil. For cases where you only have things to do when the optional is non-nil, and being nil is perfectly okay, this would allow you to pretty naturally write that code without using forced unwrapping or creating a shadowed unwrapped variable."
Lukas Stabe writes:
One thing I found myself thinking about is: How would with work when used with optionals (think failable initializers)? Would the block just not be executed (making it similar to mapping over an optional)? That would sound like a big plus for this feature, since you wouldn't need to either use optional chaining or check if the value was nil while doing further setup.
Swift Evolution sanity checks
In a preliminary straw poll for this proposal:
62% of 52 respondents were familiar with method cascading (11% neutral)
62% agreed they made method calls more fluent (30% neutral)
82% placed a priority on fluent APIs (14% neutral)
68% would use method cascades if they existed in Swift (19% neutral)
49% would object to method cascades being removed from Swift if they existed (23% netural)
64% would support adding method cascades to Swift (no neutral option offered for this question)