Skip to content

Instantly share code, notes, and snippets.

@erica
Last active January 4, 2016 16:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erica/eb32feb22ba99629285a to your computer and use it in GitHub Desktop.
Save erica/eb32feb22ba99629285a to your computer and use it in GitHub Desktop.

Adding Method Cascades

  • Proposal: TBD
  • Author(s): Erica Sadun
  • Status: TBD
  • Review manager: TBD

Current Straw Poll Proposal state is in limbo and has been redirected to this bug report

Introduction

Method cascades offer a (primarily) procedural counterpart to functional chaining. In the latter, which already exists in Swift, partial results are passed from one step to the next. In the former, the object scope is maintained through a series of sequential calls. Method cascades can return a value, so while their component calls may be procedural, the self value can be passed through to the caller.

Rather than re-invent the wheel, here is some existing motivation for such a feature in the context of Dart and Python:

To adapt the gnuplot example from the Python feature request, a Swift implementation might look like:

var plot = GnuPlotType() 
with plot {
    xrange = 0..<5
    yrange = 0..<20
    addSeries("Linear", [1, 2, 3])
    addSeries("Quadratic", [1, 4, 6])
    run()
}

This example is purely procedural example with no return values assigned to plot at the end. The method cascade that streamlines the calls to manipulate the new instance and calls functions on it. You can imagine, as well, a functional version that returns the same type T as the self that is bound using with:

with let plot = GnuPlotType() {
    xrange = 0..<5
    yrange = 0..<20
    addSeries("Linear", [1, 2, 3])
    addSeries("Quadratic", [1, 4, 6])
    run()
}

Sanity Check

A few quick points, if this feature were already in Swift, would I be using it? Definitely. Would I fight a proposal to remove it from the language? Almost certainly. Do I feel it fits the existing patterns of Swift fluency? Yes.

Motivation

Since working with Swift, I've noticed that many Apple-supplied classes, often fail to fully set up an instance for use. Here's one example that uses NSTask.

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

The launch path, the arguments, and the output pipe must be manually assigned after the declaration. In this example, the next few steps are invariably to launch() and then waitUntilExit(). These are perfect examples of non-initializer calls that would benefit from cascading, especially for a cascade that returned a value for variable binding to task.

Here's another common scenario using UILabel.

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

Without some kind of language change, you end up with stodgy repetitive code that turns into a blocky hard-to-follow clump.

  • This set-up code feels unnecessarily redundant, with the task and questionLabel symbols overwhelming and drawing attention from the actual set-up these lines of code are intended to perform.
  • The extra symbol verbiage goes against common Swift style. For example, when the context is clear, one should prefer .whitespaceAndNewlineCharacterSet to NSCharacterSet.whitespaceAndNewlineCharacterSet.
  • If you have many instances to set up there's no clear way to differentiate unrelated set-up groups other than inserting whitespace gaps or building custom factory functions. A braced method cascade could bypass these issues.

Method cascades like those in Smalltalk and Dart, cascades could alleviate these issues. In a method cascade, multiple methods can be called on the same object, for a more fluent interface. 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.

Although I tried to come up with a similar initialization example for Enumerations and Structure value types, I couldn't find anywhere in my code that was really needed as these items typically provide full set-up through built-in and convenience initializers. That said, I found many circumstances in which I repeatedly used the same item in sequential, blocking steps, which would benefit from method cascades.

Proposed solution

Method cascades automatically introduce self-references for the primary object. For example the NSTask() initialization might look something like this instead:

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

In this example, the braces are scoped to the instance as self, enabling the properties to entirely drop their prefixes and be grouped together for set-up. You could extend this to include the calls that execute the task and wait for its completion:

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

I first mocked up a similar system for playgrounds This implementation creates a pass-through closure that performs set-up on an object and then returns that object:

infix operator •-> {}

// prepare class instance
func •-> <T>(object: T, f: (T) -> Void) -> T {
    f(object)
    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

A slight tweak can add value type support but with minimal further benefits:

func •-> <T>(object: T, @noescape f: (inout T) -> Void) -> T {
    var newValue = object
    f(&newValue)
    return newValue
}

Advantages of Method Cascading

The advantages to method cascading are as follows:

  • fluent sequential calls that match the simplicity of functional chaining
  • code is streamlined, a general Swift ideal
  • can be used to add additional setup tasks for initialization
  • the indented scope provides a visual emphasis of the single task being addressed allows easier top-level initialization for global values, especially for Swift playgrounds.
  • the cascades can be extended for more general use for serial calls that do not lend themselves naturally to functional chaining.
  • could be extended for optionals as an alternative to if-let binding. 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."

Complications of this proposal

Potential complications would include:

  • the need for a new keyword such as with as suggested by Sean Heber or operator like Joe Groff's Dart-inspired .. I have not had sufficient time to cover the double-dot operator but it may play an important role, as Alex Lew points out, especially when working with trailing closure calls. Tino Heth would prefer a single dot operator followed by a closure over the keyword approach.
  • compiler tweaking so self-references within the closure work without constant copies for value types.
  • compiler adjustment to check for possible symbol conflicts, forcing self prefixes for any conflict.

The implementation must disambiguate any symbol conflicts that could break code, for example, given the following code, as pointed out by Michel Fortin.

let launchPath = "a.out"
let description = "blah blah"
with let task = NSTask() {
    self.launchPath = launchPath // would require mandatory checking for where self is needed
    arguments = [launchPath, self.description] // is that task.description?
}

Without self differentiation, it can also lead to code breakage, if for example, in the next OS release NSTask gets a new "path" property:

let path = "a.out"
with let task = NSTask() {
    launchPath = path
}

One proposed solution would be to mandate a period prefix for self-references, disambiguating while allowing developers to drop the self prefix: Joe Groff points out that a leading . is syntactically ambiguous and banned from Swift.

About Optionals

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.

About Partial Configuration

Lukas Stabe writes:

Another question for me is this: Say I want to create an instance of a class, configure it, and then assign it to a property on some other object (it can’t be assigned before it’s fully configured). Could we adapt this proposal to handle that case, too (e.g. with obj.prop = MyClass() { … })? I think it might feel more natural to move the with after the assignment (e.g. `obj.prop = with MyClass() { … }, so it could be used not just to assign to a local variable but in any context you want to use it, including method calls etc.

Disadvantages to this proposal

Adding significant new features to a language should be undertaken cautiously. Method cascading is not widely used in many programming languages although I believe it would offer a positive and immediate advantage to Swift users working with Cocoa libraries.

Arguments against this proposal

Jacob Bandes-Storch writes:

It seems like setting properties just after init is the main use case here.

I'm not against this idea, but I want to point out that this doesn't need to be solved by a change to the language. You can easily define a convenience init for UILabel that takes textAlignment, font, text, and numberOfLines as parameters. They could have default values so you can specify just the ones you need.

I like the idea of being able to do configure objects/values conveniently, but I'm not sure how to justify a language change for it. Perhaps we just need better autogeneration of initializers during Obj-C header import.

Impact on existing code

I see this initialization strategy as offering a positive way to refactor code bases for greater readability and simplicity.

Alternatives approaches

One proposal suggested by David Waite involves binding self to closure parameters. Instead of:

with let task = NSTask() {…} 

you could transform ‘with' into standard function:

func with<T>(argument:T, apply:(T) throws->()) rethrows -> T {
    try apply(argument)
    return argument
}

let task =  with(NSTask()) {
     $0.launchPath = …
}

let task =  with(NSTask()) {
    self in
    launchPath = …
    arguments = …
    standardOutput = ...
}
@wcatron
Copy link

wcatron commented Jan 4, 2016

@js raises a valid point with trailing closures. However, I'm sure there's a argument for breaking that functionality as its already a convenience feature. Furthermore the readability can be unclear as it allows developers to ignore labels and have closures with no apparent placement. I understand a lot of people like trailing closures but they could be dropped or at least require some sort of syntax.

Perhaps this proposal could go further and say that

var task = NSTask() // or passed through a parameter
var launchPath = "/usr/bin/mdfind" // Example of overlapping name.
task {
    task.launchPath = launchPath
    arguments = ["kMDItemDisplayName == *.playground"]
    standardOutput = pipe
}

Could this rename not reassign self but instead make all of task's properties global (I believe this is the correct terminology) within the scope. Overlapping names would be explicitly accessed using task. and the existing scope would take precedence. As in the example launchPath was defined in the previous scope so task must be accessed explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment