[swift-evolution] [Pitch] Richer function identifiers, simpler function types
Alex Hoppen
alex at ateamer.de
Wed May 4 09:43:48 CDT 2016
Sorry for answering this late, but I think this is a great proposal and would like to see especially the `foo(_)` syntax up for review, as it came up twice already while Doug Gregor and I discussed the implementation of getters and setters for #selector (here <https://bugs.swift.org/browse/SR-1239?focusedCommentId=13980&#comment-13980> and here <https://github.com/apple/swift-evolution/pull/280>) and the upcoming proposal for disallowing arbitrary expressions inside #selector (Link to proposal <https://github.com/ahoppen/swift-evolution/blob/arbitrary-expressions-in-selectors/proposals/0000-arbitrary-expressions-in-selectors.md>). I think not being able to reference an overloaded method without parameters without using `as` to disambiguate by type is a major hole in the type system.
Some comments inline.
> 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.
A huge +10 on this one as it stands. I think in the context of functions the underscore already has a meaning of "there is nothing" as in the parameter names. The only possible issue I see is whether we may end up in a conflict should we ever decide for functions to have out-only parameters that you may ignore by passing "_" as argument. But I don't see this coming. Only opinions from the core team?
> 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(_)`
I’m slightly opposed to this one. I, for my part, would expect `Bar.baz(_)` to refer to the static function instead of `Bar`, since nothing in this name suggests a instance methods. The fact that you can access instance methods on a type as `(Type) -> (Args) -> ReturnValue` has always seem more like magic to me.
> 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.
I think this is largely impacted by the future direction of Swift on whether argument names are counted as part of a function’s name or not. I think they currently aren’t but if I recall correctly there are thought to change this. If this is the case, removing the option to use `foo` to refer to `foo(x:)` or `foo(_)` would only make sense from my point of view and should be done in the Swift 3 timeframe as a source breaking change. Otherwise I see no point in removing the option to reference a method by its base name, because technically speaking, it’s simply its name.
Could maybe someone of the core team comment on the future direction, Swift should take?
> 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) }`.
Sounds very reasonable to me. Especially, I think the idea of creating a new function that simply forwards to the original function solves the problem of handling unspecified default parameters very elegantly.
At first I was worried about the additional indirection and its potential performance implications, but functions with default parameters already dispatch another function call for each argument anyway, so this shouldn’t be a problem.
> 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).
AFAIK, currently they are. See my comment on C3 for this.
> 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.
Again implied by the decision on whether arguments (and their names) are counted as part of the function’s signature or not.
> 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.)
I can’t really see where this implication comes from and I’m strongly against it. I wouldn’t expect value of a variable to change just because I assign it to a new variable. Neither would I want my function’s signature to change just because I assign the function to another variable.
I rather think that it’s a missing feature that closures cannot have named arguments, but that’s orthogonal to this proposal.
> 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.
All function arguments used to be one tuple, but it turned out that certain features (inout params and varargs, if i recall correctly) cannot be handled if a function is considered as only taking one tuple as an argument. Tuple splatting was removed because it didn’t fit into the language naturally anymore.
– Alex
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160504/9966569a/attachment-0001.html>
More information about the swift-evolution
mailing list