Trailing-Closure Blog Post

I am researching a blog post in which I will argue that trailing closures sometimes are not conducive to maximum code clarity and maintainability. To that end, I would like to ask this forum a couple questions about trailing closures. First, what language, if any, inspired their inclusion in Swift? I heard Ruby, but I don’t have confirmation of that. Second, why do folks use them? Some reasons I can think of are terseness, not having to include the argument label or closing paren, and desire to follow the prevailing practice.

Perhaps you should consider whether the closure is being used procedurally or functionally in your writeup. I follow @Lily_Ballard's lead, trying to restrict them to procedural applications.

2 Likes

I use them when I want to convey I’m doing something like what a classical loop does with a braces wrapped block of code.

The biggest problem from my point of view is that ‘break’, ‘continue’, and ‘return’ do not act as the reader would expect. If that were fixed, I think trailing closures would become far more useful and transparent.

1 Like

I think you're missing the obvious reason to use it, and probably the main motivating factor for implementing trailing closure syntax in a language: it allows you to make custom constructs that look like native control flow. This allows libraries to extend the language in a natural way, e.g. in the Dispatch module, without having special support in the compiler. In this sense they serve a similar purpose to operator overriding and custom operator definitions. As @AEC says, though, it would complete the picture if support was somehow extended to break/continue/return, like it is in some forms of Ruby blocks.

9 Likes

How might break be used in practice? Stop a map or forEach?

Agreed that trailing closures make Dispatch more ergonomic.

Interesting analogy with operator overloading. Both have benefits but can increase cognitive load.

Are you sure it makes sense to come up with an argument, and come up with supporting points after the fact? Seems backwards.

7 Likes

but now isn’t that how all arguments are fought?

2 Likes

I haven't used Ruby lately, but it supports two "trailing closure" formats:

arr = [1, 2, 3, 4, 5]

# "verbose" style (start with 'do', end with 'end')
arr.each do | element |
  puts element  
end

#terse style (start with '{', end with '}')
arr.each { | element | puts element }

This to me is very readable, and in fact when I was first learning Swift closures I screamed at the first example of one, including it inside the parenthesis. I calmed down after reading further, discovering the trailing closure syntax. I never use the normal "parameter" style.

1 Like

I wouldn't go as hardline as to say one should "never" use the regular style. It's useful in some cases where the parameter label provides key detail, such a first(where:), min(by:), drop(while:).

As a hard and fast rule, I suggest against hard and fast rules. Take a more nuanced position.

1 Like

I didn't express it as a rule. Just my preference. :grinning:

1 Like

The idea for the blog post sprang from my annoyance at the reduction in clarity that trailing closures, in particular their omission of the parameter name, can cause. My primary goal in posting here was to determine whether trailing closures have some benefit I was not aware of. In light of this thread, the gravamen of the post has changed from “trailing closures considered harmful” to “trailing closures considered useful in some circumstances”.

Generally my approach has been to use trailing closures when the closure can change or capture state. And use the parameter form when the closure is not actually a “closure”, but just a pure anonymous function. The terseness beyond trailing vs other is really dependent on the work being done.

1 Like

This is largely a result of my early work on Swift, but there was never any pushback along the years as other folks joined on.

For my part, the original driving reason was to be able to implement "control flow like" structures in the standard library. If you go all the way back, you'll see that I was originally trying to implement if and other statements in the standard library, and this led to some wacky stuff (e.g. overloading juxtaposition) that was eventually abandoned.

Besides that, I was aware of Ruby, but the bigger issue was the Objective-C design pattern that encouraged blocks to be the last argument, and the goal to make that feel more natural and nicer.

That said, the actual closure syntax iterated a bunch, the first recorded entry in the changelog talks about it. we went through pipe syntax and other experiments as well.

-Chris

14 Likes

Josh, tracing the story further back:

This is certainly looks like the motivation for trailing closures in Ruby, where the .each method is the preferred iteration idiom. (Ruby has a for loop too, but it’s semi-broken and generally frowned upon.)

I don’t know the history of each in Ruby, but it’s a pretty good bet it traces right back to Smalltalk, which did exactly what Chris describes here:

Smalltalk:

foo < bar
  ifTrue: [^'foo smaller']
  ifFalse: [^'bar smaller]

Here foo < bar returns a Boolean, and we are send it an ifTrue:ifFalse: message. That sounds tantalizingly like Objective-C (not a coincidence!), but [ ] are block delimiters, not method invocation delimiters. Smalltalk doesn’t specially delimit method invocations at all:

self.listPane().parent().color(Color.black())  // Swift-like

self listPane parent color: Color black  // Smalltalk (no parens needed!)

You only need parens if you need to override the default binding precedence of method invocations. When you use parens, you group them around the whole method invocation (receiver included) because they are just normal expr-delimiting parens and not some special method invocation syntax:

stuff includes: foo ifTrue: [...]    // → stuff.includes(foo, ifTrue: { … })

(stuff includes: foo) ifTrue: [...]  // → stuff.includes(foo).ifTrue({ … })

This means that in Smalltalk, which pioneered this kind of control flow by message passing, there is no special distinction necessary for trailing closures; the last argument just runs on past the end of the invocation with no intervening delimiter that needs balancing.

Ruby was directly and heavily influenced by Smalltalk, but decided to use the more familiar receiver.method(args) syntax. I don’t know whether it was the very first language to introduce a special syntax for trailing closures[1], but it’s the earliest I’m aware of.

[1] In Ruby they’re blocks, which are related but not equivalent to closures. Same spirit though; the history clearly runs through them.

6 Likes

FWIW, my personal preference for trailing closure syntax comes from Obj-C experience. In that language, it always seemed to take a ridiculously large amount of effort to get the ordering of "}" and ")" symbols correct at the end of the closure — even though I knew that I knew what I was doing. It got much worse if there were multiple levels of nesting (with ")" usually).

On top of that, it also seemed to take a ridiculously large amount of effort to decide on the best formatting of the nested syntax, since there's no good way to do it in Obj-C that also makes the relationship between the parts entirely clear.

If you are lucky enough to have a mind that handles nesting syntax easily, then you've earned the jealousy of the 90% rest of us for whom Swift's lack of nesting is a life-saver.

Disclaimer: All percentages used in this post are fictional, and bear no relationship to real percentages.

4 Likes

Three thumbs up to @QuinceyMorris last response!

1 Like

Wrapping curly braces inside of parens is both redundant and ugly (to me). The simplicity of the swift closure syntax is almost sublime. Why mess it up with parens? Being able to use it simply in function calls makes much sense (to me).

swiftlint has a rule to prefer trailing closure syntax. It has another rule to not prefer trailing closure syntax in cases where there's more than one closure parameter.

Xcode's autocomplete doesn't use trailing closure syntax so I have to always fix them up manually or wait for swiftlint to do it.

1 Like

It does, but sometimes it randomly chooses not to. One of the oldest bugs in Xcode’s Swift support.

@phoneyDev I agree with your points, but if I’m reading code that uses APIs I don’t know by heart, I would prefer to see closure-argument labels for clarity. Examples of APIs I do know by heart include filter, forEach, and async.

The fact that a significant portion of my workday consists of reading code that I didn’t write informs this preference.

Based on the thoughtful replies to my original question, I now have enough material for a blog post on both the origins of and motivations for using trailing closures.

Those parameters are usually called completion, handler, or completionHandler, which may not be too helpful. While I see your point I always use terminal closure syntax.

Actually I get annoyed when reading someone else's code where that 'completion:' parameter is present with the paren at the end. It just seems pointless and less elegant.

Terms of Service

Privacy Policy

Cookie Policy