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(_:)                 // error: ambiguous function reference. 
Could be 'Int -> Int' or '(Int, Int) -> Int'
    let f6 = foo(_:) as Int -> Int   // #13 Function reference to #3. Has type 
'Int -> Int'.
    let f7 = foo(_:) 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(_:)     // '[Int] -> Int -> ArraySlice<Int>'

    let g1 = [Int].dropFirst(_:) // '[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

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to