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 = ...
}
@Canis-UK
Copy link

Canis-UK commented Dec 8, 2015

First, some more narrowly-focused comments:

Python's with statement is somewhat different from what we're discussing here, as Python is a GC language, and much of its with machinery is about enforcing bounds on the lifetime during which a resource is "held" (e.g. a file is kept open, a mutex is kept locked) in an environment when objects live on until the GC decides it is done with them, and dealing with edge cases (e.g. exceptions, which Python has), making it a much more complex thing to wrap one's head around.

It doesn't do anything to self, instead binding explicitly to another name, something we can already do with closures.

Whereas, really, rebinding self is almost all this Swift proposal is suggesting.

Which isn't a criticism of the proposal — I think rebinding self is useful in a surprising number of situations. For initialisation, as described here, but also for a wide variety of other tasks.

Regarding terminology:

  • with is probably the right term for it: you are doing a bunch of stuff "with" the named item. I'd be happy with anything that suggests a context shift within the scope, but with is crisp and seems to make sense in a variety of use-cases.
  • implicit sounds more technical and so we might be inclined to think that it's more precise — but I believe it obscures the meaning rather than adding to it. Many things are or can be implicit, so if we were to go that route, it would need to be even wordier to explain what is implicit (e.g. implicit self = ... — although that makes the syntax around the functional form even more complicated).
  • Talking of which, I agree with Lukas that with goes after the assignment and (whether it's implemented as a keyword or as a stdlib function) is function-like. Being an actual stdlib function seems more Swiftian.
  • I think I prefer David's version overall, although there are clearly readability/maintainability concerns with arbitrarily rebinding self. Nonetheless I think it's useful enough to outweigh those concerns — for far more than just initialisation — but there might need to be something to mitigate those concerns, by either restricting the situations where it can be done for the sake of sanity, or visually flagging up what's going on. (self in helps with that a bit. I almost kinda want Xcode to tint the background of self in blocks slightly differently, but that's out of scope for Swift itself of course...)

Now for some bigger-picture observations:

Broadly speaking, I like it because it not only simplifies and clarifies object initialisation, it also allows you to set up a temporary namespace of methods, constants and variables and the operators that act upon them, with the convenience of globals, but without actually polluting the global namespace and all that implies in terms of non-locality or "mystery meat". Unlike simply importing into a namespace (like C++'s using, say, or import in Python inside of methods), it also brings with it a context that can be acted upon or where results can be gathered. This makes all the difference in the world. A couple of examples...

Constructing HTML templates by defining methods on a class that, when called, append the supplied contents, wrapped in those tags, to a buffer inside the class instance:

let items = ["one", "two", "three"]
let htmlSnippet = with(HTMLBuilder()) {
    self in

    H1("Hello World")
    P("A paragraph of text")
    UL {
        for item in items { LI(item) }
    }
}

Compiling shaders at runtime (inspired by Cartography's use of proxies and operators within closures to construct autolayout constraints):

let shader = with(PixelShader(uniforms: ["sprite": .Sampler2D, "textureCoord": .Vec2.Low, "color": .Vec4.Low])) {
    self in

    // wonder if there's a better way of mapping these?
    let sprite = uniform["sprite"]
    let textureCoord = uniform["textureCoord"]
    let color = uniform["color"]
    fragmentColor == Texture2D(sprite, textureCoord.xy) * color
}

In the general case, defining global methods with near-meaningless names like P or overriding the == operator to do something that is not, in fact, equality-checking, is pretty gross (confusing, highly mystery-meat-y, etc). However, in these cases, they're not global, but hygienically restricted only to certain explicitly-defined scopes which make their meanings transparently clear to the reader (or at the very least flag up that this scope is unusual enough to require further investigation!). Like languages that allow inline assembly code, or HTML templates that have syntax for embedding code from a scripting language, you're providing an escape valve and in effect breaking out into your own DSL with its own rules.

Which is fine, and useful, and something people are going to do anyway even if it's just by writing their own DSL from scratch and parsing it from strings or plists or xml, only then they lose all the type-checking, compile-time verification & speed advantages and have to reimplement everything and jump through hoops to integrate the two languages and urgh please no not again.

I guess what I'm arguing is that this construct makes code "cleaner" rather than "dirtier" because it takes potentially-confusing idioms, operators etc that people are likely to use anyway, and "quarantines" them inside clearly-marked closures, instead of sprawling them across global scope or arbitrary objects (in a similar way to how Cartography does not add custom operators or properties to UIViews, but instead creates hygienic proxies for them that only exist inside particular closures).

I could see it being used for defining other stuff like SQL statements, Core Data predicates, test cases (David's original use-case, I believe), some kind of string-matching that was like regex but actually readable and maintainable, animation sequences, css-like view styling... all sorts of things, but in particular things where you're bridging the gap between code and data: what you have is data-like but more dynamic/you want something that looks like a series of imperative statements but is actually constructing data from it.

An objection I can see being raised is that some of these things can also be done more "functionally", a popular technique being to create a context object first, then call a method on it, and each method returns a new context with the change applied, and you stack up a long series of dotted method calls, HTMLBuilder().This().That().The().Other(). This works but can (IMO) get tangled and hard to read when you need to mix in anything that isn't of functional idiom, such as assigning a temporary variable, part way through the chain, especially when the assignment isn't the current state of the chain itself. (Just one example.)

Another objection is that rebinding self has been a problem in other languages such as JavaScript. However, I believe the issue there is not rebinding self (this in JS) per se, but that it gets rebound for you, behind your back and (according to this article about this) can sometimes, on some browsers, be impossible to know what it will be bound to, and therefore leads to confusion and mystery-meat. David's suggestion sidesteps this issue by explicitly declaring the rebinding at the point where it applies, inside the closure.

Another objection is that you can simply bind to some other name and use it explicitly. In theory, this aids clarity, and requires no language changes, but I'd simply point to the HTML example as a counter to this. Imagine a whole page of HTML full of extraneous html. prefixes before Every. Single. Tag. All that visual noise and meaningless repetition!

@js
Copy link

js commented Dec 18, 2015

How would this look if the initializer takes a trailing closure argument, eg init(handler: () -> Void))?

To a newcomer I'd imagine the syntax looks very similar to initialization with named parameters, I think the difference is slightly awkward to explain, unless they've experience the receptiveness of simply typing the naming of the variable they're calling methods/properties on.

Dart's .. avoids some of these things.

@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