Skip to content

Latest commit

 

History

History
197 lines (147 loc) · 7.71 KB

0066-standardize-function-type-syntax.md

File metadata and controls

197 lines (147 loc) · 7.71 KB

Standardize function type argument syntax to require parentheses

Introduction

Function types in Swift use parentheses around their parameter list (aligning with the function declaration syntax, as well as the syntax used to call a function). However, in the degenerate case of a single non-variadic, unlabeled argument with no attributes, Swift allows the parentheses to be omitted. For example, these types:

(Int) -> Float
(String) -> Int
(T) -> U
(Int) -> (Float) -> String

May be written as:

Int -> Float
String -> Int
T -> U
Int -> Float -> String

While this saves some parentheses, it introduces some minor problems, is not consistent with other parts of the Swift grammar, reduces consistency within function types themselves, and offers no additional expressive capability (this is just syntactic sugar). This proposal suggests that we simply eliminate the special case and require parentheses on all argument lists for function types.

Swift-evolution thread: [pitch] Eliminate the "T1 -> T2" syntax, require "(T1) -> T2"

Motivation

Allowing this sugar introduces ambiguities in the language that require special rules to disambiguate. For example:

() -> Int           // Takes zero arguments, or takes one zero-argument parameter?
(Int, Float) -> Int // Takes two arguments, or takes one two-argument tuple?

This syntactic sugar reduces consistency with other parts of the language, since declarations always require parentheses, and calls requires parentheses as well. For example:

func f(a : Int) { ... } // ok
func f a : Int { ... }  // my eyes!

Finally, while it is straight-forward to remove this in Swift 3 (given the other migration that will be necessary to move Swift 2 code to Swift 3), removing this after Swift 3 will be much harder since we won't want to break code then. It is now or never.

History

The original rationale aligned with the fact that we wanted to treat all functions as taking a single parameter (which was often of tuple type) and producing a single value (which was sometimes a tuple, in the case of void and multiple return values). However, we’ve long since moved on from that early design point: there are a number of things that you can only do in a parameter list now (varargs, default args, internal vs API labels, etc), we removed implicit tuple splat, and the compiler has long ago stopped modeling function parameters this way.

Proposed solution

Parentheses will be required in function types. Examples:

Int -> Int           // error
(Int) -> Int         // function from Int to Int
((Int)) -> Int       // also function from Int to Int

Int, Int -> Int      // error
(Int, Int) -> Int    // function from Int and Int to Int
((Int, Int)) -> Int  // function from tuple (Int, Int) to Int

let f: () -> Int     // function with no parameters
let g: (()) -> Int   // function taking a single () parameter
let h: ((())) -> Int // function taking a single () parameter

f();   g(()); h(())  // correct
f(()); g();   h()    // errors

Function type grammar

Parentheses will become a part of function type grammar:

function-type( function-type-parametersopt ) throws-annotationopt -> type

function-type-parametersfunction-type-parameter , function-type-parameters

function-type-parametersfunction-type-parameter ...opt

function-type-parameterattributesopt inoutopt type

throws-annotationthrows | rethrows

Impact on existing code

The migrator will automatically add parentheses to existing code when moving from Swift 2 to Swift 3.

Related questions

This proposal is very simple and well scoped, but in discussion, several follow-on questions have asked about what precedent this sets - if we change this, then what else would align to it. While we cannot predict the future of where the Swift community will want to go, this section states the opinion of the author on these topics.

Should function return types be parenthesized?

In my opinion, no. Unlike arguments, there is no precedent already in Swift that leads to the result type of functions being parenthesized (e.g. in declarations). The result of a function also does not have any of the magic and complexity of parameter lists: it really is just a type.

Finally, in terms of ergonomics, the return type of a function is very commonly written in code - almost every function and method has one. In contrast, function types are very rarely written - typically only when writing higher order functions.

Should we require parentheses in closure expression parameter lists?

In my opinion, no. Swift currently supports a number of syntactic shortcuts in closure parameter lists, which are important for expressiveness of simple functional algorithms. For example, very few people write out this long-form expression to sort an array of integers backward:

y = x.sorted { (lhs : Int, rhs : Int) -> Bool in rhs < lhs }

Many people use:

y = x.sorted { lhs, rhs in rhs < lhs }

Or they use the even shorter form of { $1 < $0 }.

Some folks have asked whether it would make sense to start requiring the parentheses around the parameter lists for consistency with function types. However, note that this is structurally a different kind of syntactic sugar: you are allowed to elide the parens even when you have multiple arguments, you are allowed to omit the return type, you are allowed to omit the types, and you're even allowed to omit the parameter list in its entirety. Short of a complete rethink of closure syntax (something that I'm not suggesting - I'm personally very happy with our closure syntax!), requiring parentheses here would not improve the language in an apparent way.

Common objection

The most common objection to this proposal cites a reduction in clarity for higher order functions that take one parameter. Consider a (simplified) implementation of map for example, written with the parentheses:

extension LazySequenceProtocol {
  /// Returns a `LazyMapSequence` over this `Sequence`.  The elements of
  /// the result are computed lazily, each time they are read, by
  /// calling `transform` function on a base element.
  func map<U>(_ transform: (Elements.Iterator.Element) -> U) -> LazyMapSequence<Self.Elements, U>
}

The author is unconvinced by the claims that requiring parentheses on the transform parameter unacceptably reduce readability. Consider:

  • Many higher order functions are generic, which mean that they often take long names like Element (where the parens do not add much clutter), or an excessively short name (e.g. T) where the parentheses add structure.

  • The claims of "parentheses blindness" are a possible issue, but they help offset the similar issue of "arrow blindness", as demonstrated by the example above.

Further, the declaration of a higher order functions is very rare (use of one is much more common, and is unaffected by this proposal), so it is not worth deploying sugar to syntax optimize. If Swift 1 required parentheses on function types, we would almost certainly reject a proposal to syntax optimize them away.