[Pitch] Richer function identifiers, simpler function types

(James F) #1

This is a great idea!
I'd love to see this in Swift 3.

------------ Begin Message ------------
Group: gmane.comp.lang.swift.evolution
MsgID: <EBAF6231-ECB0-483B-A647-3629A7DAB44A@iki.fi>

This is another reaction to SE-0066 <https://github.com/apple/swift-evolution/blob/master/proposals/0066-standardize-function-type-syntax.md> to which I'm mildly against.

I'd like to propose the following language changes to simplify function types and clarify what a function's name is. What gets removed is already ambiguous. And what is added makes higher-level programming with functions considerably simpler than currently. Furthermore, the proposed change considerably limits what constitutes the overload set of a function, which probably also speeds up the compilation process.

Let's consider the following declarations:

    func foo() // #1 Function named 'foo(_)' with type '() -> ()'.
    func foo(x: Int) -> Int // #2 Function named 'foo(x:)' with type 'Int -> Int' (not an overload).
    func foo(_ x: Int) -> Int // #3 Function named 'foo(_:)' with type 'Int -> Int'
    func foo(_ x: (Int, Int)) -> Int // #4 Function named 'foo(_:)' with type '(Int, Int) -> Int' (overload of #3).
    func foo(x: Int, y: Int) -> Int // #5 Function named 'foo(x:y:)' with type '(Int, Int) -> Int'.
    func foo(x: Int, y: Int) -> Bool // #6 Function named 'foo(x:y:)' with type '(Int, Int) -> Bool' (overload of #5).
    let foo: Int // error: invalid redeclaration of 'foo' (previously declared as a function)
    let baz: (Int, Int) -> Int // #7 Variable named 'baz' with type '(Int, Int) -> Int'.
    class Bar {
        func baz() // #8 Method named 'Bar.baz(_)' with type 'Bar -> () -> ()'.
        func baz(x y: Int) // #9 Method named 'Bar.baz(x:)' with type 'Bar -> Int -> ()'.
        static func baz(x: Int = 0) // #10 Static method named 'Bar.Self.baz(x:)' with type 'Int -> ()'.
    let f1 = foo // error: not a function reference, did you mean 'foo(_)'?
    let f2 = foo as () -> () // error: not a function reference, did you mean 'foo(_)'?
    let f3 = foo(_) // #11 Function reference to #1. Has type '() -> ()'.
    let f4 = foo(x:) // #12 Function reference to #2. Has type 'Int -> Int'.
    let f5 = foo(_:slight_smile: // error: ambiguous function reference. Could be 'Int -> Int' or '(Int, Int) -> Int'
    let f6 = foo(_:slight_smile: as Int -> Int // #13 Function reference to #3. Has type 'Int -> Int'.
    let f7 = foo(_:slight_smile: as (Int, Int) -> Int // #14 Function reference to #4. Has type '(Int, Int) -> Int'.
    let x1: Int = foo(x:y:)(1, 2) // #15 Function application of #5. Picks the right overload by explicit return type.
    let x2: Bool = foo(x:y:)((1, 2)) // #16 Function application of #6. Allowing a tuple here causes no ambiguity.
    let f9 = baz // #17 Function reference synonymous to #7. Has type '(Int, Int) -> Int'.
    let bar = Bar()
    let f10 = bar.baz // error: not a function reference, did you mean 'bar.baz(_)' or 'bar.baz(x:)'?
    let f11 = bar.baz(_) // #18 Function synonymous to the closure '{ bar.baz() }' with type '() -> ()'.
    let f12 = bar.baz(x:) // #19 Function synonymous to the closure '{ bar.baz(x: $0) }' with type 'Int -> ()'.
    let f13 = Bar.Self.baz(x:) // #20 Function synonymous to the closure '{ Bar.baz(x: $0) }' with type 'Int -> ()'.
    let f14 = Bar.Self.baz(_) // #21 Function synonymous to the closure '{ Bar.baz() }' with type '() -> ()'.

The following list of proposed changes sum up what's new above.

C1: Extend SE-0021 <https://github.com/apple/swift-evolution/blob/master/proposals/0021-generalized-naming.md> by adding the underscore-in-parentheses syntax `foo(_)` to refer to the zero-argument function #1.

C2: Extend SE-0021 <https://github.com/apple/swift-evolution/blob/master/proposals/0021-generalized-naming.md> by removing the ambiguity between instance and type members. From now on, `Bar.baz(_)`

C3: Extend SE-0021 <https://github.com/apple/swift-evolution/blob/master/proposals/0021-generalized-naming.md> by banning the use of base name only to refer to a function, i.e. neither `foo` nor `Bar.baz` can be used to refer to refer to any of #1#6 or #8#10.

C4: Extend SE-0021 <https://github.com/apple/swift-evolution/blob/master/proposals/0021-generalized-naming.md> to allow the selective omission of defaulted arguments, e.g. `let f = print(_:separator:)` creates the function variable `f: (Any, String) -> ()` equivalent to `{ print($0, separator: $1) }`.

C5: Clarify the language specification by stating that functions with different labels (e.g. `foo(_:)` vs. `foo(x:)`) are not overloads of each other. Instead, two functions are considered overloads of each other if only if they have matching base names (e.g. `foo`) and matching argument labels (e.g. `(x:y:)`) but differing argument or return types (e.g. #3 and #4, or #5 and #6).

C6: Clarify that by using the base name `foo` for a function, the same scope cannot define a variable with the name `foo`. And, vice versa, in a scope that defines a variable named `foo`, there can be no function `foo(...)` defined at the same scope level.

The implications are:

I1: The use of a function's base name to refer to a function will cease to work. It has, however, been buggy up to this date. Consider the following:

    let f = [Int].prefix // '[Int] -> Int -> ArraySlice<Int>'

    let g1 = [Int].dropFirst // Inexplicably chooses the '[Int] -> Int -> ArraySlice<Int>' overload!
    let g2 = [Int].dropFirst as [Int] -> () -> ArraySlice<Int> // Disambiguate by type.

    let h1 = [Int].sorted // Chooses the '[Int] -> () -> [Int]' overload, unlike 'dropFirst' above.
    let h2 = [Int].sorted as [Int] -> ((Int, Int) -> Bool) -> [Int] // Disambiguate by type.

With the proposed changes, the above becomes:

    let f = [Int].prefix(_:slight_smile: // '[Int] -> Int -> ArraySlice<Int>'

    let g1 = [Int].dropFirst(_:slight_smile: // '[Int] -> Int -> ArraySlice<Int>'
    let g2 = [Int].dropFirst(_) // '[Int] -> () -> ArraySlice<Int>'

    let h1 = [Int].sorted(_) // '[Int] -> () -> [Int]'
    let h2 = [Int].sorted(isOrderedBefore:) // '[Int] -> ((Int, Int) -> Bool) -> [Int]'

I2: When referring to functions the argument labels disappear in the returned function. That's a good thing because there's no generic way to call functions with arguments, and that's why closures too always come without argument labels. We don't, however, lose any clarity at the point where the function reference is passed as an argument because function references always contain the labels in the new notation. (Also, see the future directions for an idea how argument labels can be restored in the function variable.)

I3: Function argument lists are no longer that special and there's no need to notate single-n-tuple argument lists separately from n-argument functions, i.e. SE-0066 <https://github.com/apple/swift-evolution/blob/master/proposals/0066-standardize-function-type-syntax.md> is not really needed anymore. The new intuition here is that it's the function's name that defines how a function can be called, not its type.

I4: Because function variables cannot be overloaded, we can without ambiguity allow all of the following "tuple splatting":

    let tuple1 = (1, 2)
    let tuple2 = (x: 1, y: 2)
    let tuple3 = (a: 1, b: 2)
    let y1 = foo(tuple1) // Not a "tuple splat", calls #4 as normal.
    let y2 = foo(tuple2) // Not a "tuple splat", calls #4 as normal.
    let y3 = foo(tuple3) // Not a "tuple splat", calls #4 as normal.
    let y4 = foo(_:)(1, 2) // Not a "tuple splat", calls the reference to #4 as normal.
    let y5 = foo(_:)((1, 2)) // "Tuple splat", calls #4.
    let y6 = foo(_:)(((1, 2))) // "Tuple splat", calls #4. Nothing special here, just an unnecessary pair of parens.
    let y7 = foo(_:)(tuple1) // "Tuple splat", calls #4.
    let y8 = foo(_:)(tuple2) // "Tuple splat", calls #4. The labelled tuple type is compatible with '(Int, Int)'.
    let z1 = foo(x:y:)(tuple1) as Int // "Tuple splat", calls #5 because the return type is explicitly 'Int'.
    let z2 = foo(x:y:)(tuple2) as Int // "Tuple splat", calls #5. The labels don't really matter here.
    let z3 = foo(x:y:)(tuple3) as Int // "Tuple splat", calls #5. Like above, any tuple labels are compatible in the call.
    let z4 = (foo(x:y:) as (Int, Int) -> Bool)(tuple3) // Here's another way to explicitly pick up the overload.

Future directions

F1: In this proposal, I made a difference between function identifier names and variable identifier names. However, it could be well allowed to use function-like names for function variables when clarity is needed. Then calling a block would require the argument labels just like functions:

    let block(value:) = {value in ...}
    block(value: 1)

    func foo(_ isOrderedBefore(lhs:rhs:): (Int, Int) -> Bool) {
        let x = isOrderedBefore(lhs: 1, rhs: 2)

F2: The following idea probably divides opinions, but because function variables are unambiguous, we could even allow calling them without parentheses like in Haskell. That would open up many doors to currying and building highly composable functional libraries:

    let f = {x in ...}
    let y = f 1 // calls 'f' with an 'Int'
    let z = f (1, 2) // calls 'f' with an '(Int, Int)'

F3: And if that was allowed, maybe it could be possible to define functions with variable-like names and currying too, but now I'm getting too far. I think the proposal is worth considering even if we never go to the direction of Haskell in this way.

— Pyry

------------- End Message -------------

From James F