Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-160] Proposal to introduce Method Cascading #42782

swift-ci opened this issue Dec 9, 2015 · 1 comment

[SR-160] Proposal to introduce Method Cascading #42782

swift-ci opened this issue Dec 9, 2015 · 1 comment
compiler The Swift compiler in itself feature A feature request or implementation


Copy link

swift-ci commented Dec 9, 2015

Previous ID SR-160
Radar None
Original Reporter erica (JIRA User)
Type New Feature

Attachment: Download

Additional Detail from JIRA
Votes 10
Labels New Feature, LanguageFeatureRequest
Assignee None
Priority Medium

md5: fc409db31758e2098b21c63dd306eca1

Issue Description:

Adding Method Cascades
Proposal: TBD
Author(s): Erica Sadun
Status: TBD
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.

Related Reading

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.

let task = NSTask()
task.launchPath = "/usr/bin/mdfind"
task.arguments = ["kMDItemDisplayName == *.playground"]
task.standardOutput = pipe

This build-then-specialize pattern extends throughout Cocoa/Cocoa Touch. This next interface-building example uses UILabel.

let questionLabel = UILabel()
questionLabel.textAlignment = .Center
questionLabel.font =  UIFont(name:"DnealianManuscript", size: 72)
questionLabel.text = currentQuestion.questionText
questionLabel.numberOfLines = 0

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:

let task = NSTask()
task.launchPath = "/usr/bin/mdfind"
task.arguments = ["kMDItemDisplayName == *.playground"]
task.standardOutput = pipe

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.
Proposed solution

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:

instance.expression1; instance.expression2; instance.expression3; ...

to either a non-binding version:

with instance { expression1; expression2; expression3; ... }

or a binding version:

with let symbol = value { expression1; expression2; expression3; ... }

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:

with let task = NSTask() {
    launchPath = "/usr/bin/mdfind"
    arguments = ["kMDItemDisplayName == *.playground"]
    standardOutput = pipe

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.

Reference conflicts

Binding into a new scope introduces potential reference conflicts. Consider the following example:

class MyClass {
   var sharedName: ... (1)

   func something() {
       var sharedName: ... (2)

       with var myInstance: SomethingWithAnSharedNameProperty { (3)
          sharedName = newValue // sharedName is myInstance (4)
          sharedName = self // myInstance.sharedName is myInstance (5)

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:

class MyClass {
   var sharedName: ... (1)

   func something() {
       var sharedName: ... (2)

       with var myInstance: SomethingWithAnSharedNameProperty { (3)

          // Symbol resolution
          sharedName = newValue      // myInstance.sharedName = newValue (3)
          _.sharedName = newValue    // the locally scoped newValue (2)
          _._.sharedName = newValue  // I don't actually propose this, but (1)

          // Self resolution
          self.doSomething()   // self is myInstance
          _.self.doSomething() // self is an instance of MyClass

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:

let path = "/bin/ls"
with let task = NSTask() {
    launchPath = path

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.

Alternatives considered

Method cascading can be approximated in current Swift using custom operators, for example:

infix operator •-> {}

// prepare class instance
func •-> <T>(object: T, f: (T) -> Void) -> T {
    return object

// e.g.
class MyClass {var (x, y, z) = ("x", "y", "z")}
let myInstance = MyClass() •-> {
   $0.x = "NewX"
   $0.y = "NewY"

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)

Copy link

I originally posted this to evolution, but figured it should be captured here.

An alternative approach would be if Swift could adopt what Kotlin calls "extension function expressions". These would allow us to encode a closure's receiver type (self) into the closure type itself, providing a powerful, type-safe way to define DSLs.

With extension functions, we could approach something similar:

func with<T>(value: T, body: T.() -> ()) -> T {
    return value

let task = with(NSTask()) {
    launchPath = "/usr/bin/mdfind"
    arguments = ["kMDItemDisplayName == *.playground"]
    standardOutput = pipe

Or, using the then pattern:

protocol Builder {}
extension Builder {
    func then(body: Self.() -> ()) -> Self {
        return self
extension NSTask: Builder {}

let task = NSTask().then {
    launchPath = "/usr/bin/mdfind"
    arguments = ["kMDItemDisplayName == *.playground"]
    standardOutput = pipe

How about BDD-style frameworks (like Quick, Nimble and Spectre)?

describe("a person") {
    let person = Person(name: "Kyle")

    it("has a name") {
        try expect( == "Kyle"

And HTML builders?

html {
    head { title("Hello, World!") }

And block-based, transactional APIs?

db.inTransaction {
    delete("users", "first_name = ?", ["Jake"])

The main benefits have been previously discussed:

  1. The ability to remove noise ("$0" everywhere)

  2. The ability to avoid defining globals (in favor of a safer, scoped interface)

A probably-incomplete list of previous discussions around this topic:

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@AnthonyLatsis AnthonyLatsis added compiler The Swift compiler in itself and removed new feature labels Nov 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
compiler The Swift compiler in itself feature A feature request or implementation
None yet

No branches or pull requests

3 participants