[swift-evolution] Streamlining closures

Jordan Rose jordan_rose at apple.com
Tue Dec 15 14:29:33 CST 2015


Hi, Taras. Thanks for writing this up. Unfortunately, it's infeasible to do most of these things, for several reasons.

> On Dec 12, 2015, at 20:14 , Taras Zakharko via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Dear all, 
> 
> first of all, thank you for giving the community the opportunity to influence the future of Swift. This is a strong move, which shows how passionate the Swift team is about their language and how you guys want to make it the best. 
> 
> Personally, I am a huge fan of streamlined, logically concise concepts as well as syntax in programming languages. This is an area where Swift both excels at (love what you do with types in the compiler) but also sometimes lacking, as there seems to be a lot of idiosyncrasy, as evident from the huge number of language keywords and compiler attributes.  Here, I would like to discuss some of the idiosyncrasy with closures. As I am trying to throw some random ideas out there, my post might be a bit chaotic, so please bear with me.  Besides, I literally started playing around with Swift only few hours ago (I know, I know, I’m late), so my practical knowledge of the language is very limited. 
> 
> 1. I always wondered about the specific closure syntax { A -> B in … }. The type annotation is within the block for some reason and the there is the keyword ‘in’, which is usually associated with the for loop. My proposal would be to change this to simply:
> 
>    A -> B { … }
> 
> That is, the closure declaration closely mirrors the function declaration, but without the 'func name’ part. This is far from being just a cosmetic change though. Rather, it has some far-reaching consequences to how the concept of closure is applied to the language, but I believe that this would make Swift much more streamlined and concise (and also solve some other issues in the process). 

We thought a lot about this, and settled on the current syntax (inside the braces) for several reasons, the main one being that it's much easier to parse. Without this, the compiler would have to stop whatever it's currently doing when it sees '->'. We do already do some of this work for things like "[Int]()" (which starts out as an array literal and then gets reinterpreted as a type later), but it leads to weird errors when the code is invalid or incomplete.

The keyword "in" was chosen because (a) it's already a keyword, (b) it's still short, and (c) it felt more Swifty than Smalltalk/Ruby pipes.

> 
> The crucial point is that the type annotation is (and should be) optional. Which means that every code block { … } is a closure. I think this fundamentally makes a lot of sense. So, a function declaration is just a closure associated with a name identifier (aka. injected into the namespace). Similarly, code blocks used in statements such as for and do have local lexical scope, which makes them essentially compatible closures as far as I can see. 

Today, 'return' from a closure cannot return from the containing function. That alone would be a big language change.


> 
> Essentially, this approach radically simplifies the language both from the syntax standpoint — closures and funcs are using the same syntax, the notion of code blocks is removed from the language; but also from the semantics standpoint. For instance, one can give more formal definitions for statements, which opens the path of treating statements as expressions (see https://lists.swift.org/pipermail/swift-evolution/2015-December/000133.html <https://lists.swift.org/pipermail/swift-evolution/2015-December/000133.html>). For instance, current if is of type (Bool, ()->())->(), but it can easily be redeclared as (Bool, ()->T)->T, which opens way to nice things like 
> 
>   let x = if(…) { .. some complex setup code..; value1} else { … other code..; value2}
> 
> Similarly, do can be used to incapsulate auxiliary computations in the local scope (see below).

As discussed on the other thread, it's not at all clear that this is desireable. The implicit return in particular would be controversial <http://awardwinningfjords.com/2012/05/08/beware-coffeescript-comprehensions.html>.

> 
> 2. Another closure limitation is that return type inference does not work with longer closures (but they should). Here a particular use case.  Due to my work, I program a lot with R (which is sometimes a neat language in its own right). A particular paradigm I find very useful is to use anonymous code blocks in local scope to prevent namespace pollution, e.g. (in R code)
> 
>  dataset <- local({
>    a <- some_computation
>    b <- some_computation
> 
>    a combined with b
> })
> 
> // a and b are not visible in the outer scope anymore
> 
> In JavaScript, people often use anonymous functions for the same effect. The nice thing about this paradigm is that it allows one to properly isolate data dependency from each other, give the compiler a better sense of variable lifespan and overall make your code much nicer to read. Of course, in Swift  this can be done with nested functions or closures, but its the question of aesthetics. At any rate, the R function is very easy to emulate in Swift:
> 
>  func local<T>(closure: () -> T) -> T {
>      return closure()
>  }
> 
> which works beautifully, e.g.
> 
>  let (a1,a2) = local({return(1, 2)}
> 
> However, if I want to use more complex code, I need to explicitly declare the closure type:
> 
>    let (a1, a2) = local({ () -> (Int, Int) in
>      let x2 = x*2
> 
>      return (x2, x2+1)
>    })
> 
> 
> This is bulky, and frankly, unnecessary, as the compiler can easily infer the return type of the closure. 
> 
> BTW, combined with proposal in 1., this style of programming just becomes the beautiful
> 
> let (a1, a2) = do {
>   .. do something ..
>  return(…, …) // or simply (…, …)
> }

Swift's type inference is currently statement-oriented, so there's no easy way to do this inference. This is at least partly a compilation-time concern: Swift's type system allows many more possible conversions than, say, Haskell or OCaml, so solving the types for an entire multi-statement function is not a trivial problem, possibly not a tractable problem.

The canonical way to write your closure would be this:

  let (a1, a2): (Int, Int) = {
    let x2 = x*2
    return (x2, x2+1)
  }()

To summarize, I think Swift has made the right trade-offs for a statically-typed language that prefers clarity over brevity <https://swift.org/documentation/api-design-guidelines.html#fundamentals>. Simply re-syntaxing closures isn't out of the question, but we ultimately went for something that handles many inline uses with varying amounts of type info rather than trying to stick as closely as possible to function syntax.

Best,
Jordan
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151215/d9892385/attachment.html>


More information about the swift-evolution mailing list