Should Swift be able to declare a function that matches a function type?

Hi all,
Should a future version of Swift be able to do something like this?

typealias EventHandler = (_ sender: Any?) -> Void

func keyPressed: EventHandler {

}

Can you provide more clarification on why you believe that this would be better compared to

let keyPressed: EventHandler = {// sender in
    /*...*/
}

I am having a hard time finding a use as you can conform types to have particular functions with particular signatures and can declare immutable closures. This behavior seems to be more complicated and is adding another way to do something that you can already do without any tangible benefits over other methods.

Assuming this let keyPressed is inside a class.

It will be initialized at the class instance creation, and thus will not be able to reference other class members (e.g. fooBar). The compiler says Cannot use instance member 'fooBar' within property initializer; property initializers run before 'self' is available.

OK, let's try lazy var instead of let. In this case every class member should be prepended with explicit self., otherwise compiler will complain Reference to property 'fooBar' in closure requires explicit use of 'self' to make capture semantics explicit. Not very convenient.

Functions have their use case in the language. My suggestion is to improve functions when they lack a feature, and not replace them with other language constructs.

1 Like

Signatures may be lengthy. Using a function type when declaring a function is more concise. This is especially useful if you have many functions of the same type, e.g. multiple event handlers in a GUI framework.

Signatures may change over time as code evolves, and this will lead not only to necessity to change them in multiple places, but also this will lead to hard-to-understand compiler errors in places far from the signatures themselves, e.g. if I assign a different function to a variable of type EventHandler with mismatching signature, I will get the error Type of expression is ambiguous whithout a type annotation - at variable assignment site, not at the function declaration site.

There are things you can do in the parameter list of a named function declaration that you cannot do in a function type, such as default arguments, generics, etc. So this kind of func would be quite limited in what it can express compared to a regular func.

8 Likes

Yes, but this actually means that function type declarations are currently limited. Even argument labels are not allowed.

That's because argument labels are not part of a function's type, they're just part of its name.

3 Likes

While Signatures are lengthy they also provide information to the creator of a conforming function on what they need to return and throw as well as what is being provided to the function; all without leaving the area of the code they are working on. By placing your function’s declaration in a typealias, which could be three lines away or in a separate package, you introduce complexity to parse the code.

Take this code for a onHover function.

func onHover: HoverFunc { // point, context in
    guard
        point.x >= 0,
        point.y >= 0,
        context.get() != point
    else {
        throw .badInput(point)
    }
    return (point.x / 2 * point.y, point.y, .callID(“Demo”))
}

What is the Error type being thrown, what type is context or point and what does it’s get function mean and what exactly are we returning? We would need to be aware of its signature to effectively reason about the expectations of this function. Could you even tell me if this code would work without seeing the typealias declaration for HoverFunc

Additionally by following how a typealias works, you can produce code like this

func keyPressed: (_ sender: Any?) -> Void { // sender in
    /*...*/
}

Also is this using a subset of closures or does it support every closure declaration format? For example this:

func keyPressed {(_ sender: Any?) -> Void in
    /*...*/
}
1 Like

Yes, I just realized this and corrected my initial code snippet by removing the closure-style argument list from the function body.

What you want already exists. The only difference is one extra set of nested {}s.

A method whose signature can be represented in the type system is just syntactic sugar for a computed property which returns a closure.

typealias EventHandler = (_ sender: Any?) -> Void

final class MusicClass {
  private let instanceMember = "🎹🎶"
  func keyPressedVersion1(_ sender: Any?) { print(instanceMember) }
  var keyPressedVersion2: EventHandler { keyPressedVersion1 }
  var keyPressedVersion3: EventHandler { MusicClass.keyPressedVersion1(self) }
  var keyPressedVersion4: EventHandler {{ [self] _ in print(instanceMember) }}
}
MusicClass().keyPressedVersion1(nil) // 🎹🎶
MusicClass().keyPressedVersion2(nil) // 🎹🎶
MusicClass().keyPressedVersion3(nil) // 🎹🎶
MusicClass().keyPressedVersion4(nil) // 🎹🎶
2 Likes

A method that returns a value of function type is another possibility, again only requiring an extra set of { }.

1 Like

I fully agree, but I noticed an interesting thing in Swift where it kind of violates this rule:

typealias EventHandler = (_ sender: Any?) -> Void
func myEventHandler(sender: Any?) {}
var keyPressed: EventHandler

keyPressed = myEventHandler(sender:) // works fine with full function name
keyPressed = myEventHandler // works fine with partial? function name

I think this is because Swift can infer the full function name, it is generally known for allowing the omission of things that can be inferred.

This isn't violating the rule: argument labels are still part of the function's name. However, the Swift compiler is big on inferring things and, because there are no ambiguous overloads of myEventHandler here, is able to infer the labels.

2 Likes

I believe this was for source compatibility with older code from before swift-evolution/proposals/0021-generalized-naming.md at main · swiftlang/swift-evolution · GitHub was introduced. It would be nice to phase this behavior out, because as it stands, adding a new overload -- even with different argument labels -- is a source-breaking change.

1 Like

Just as a warning, if you do this, this forms a circular reference within the class and I don't think it'll ever be deallocated, unless you say keyPressed = {} or similar somewhere else.

2 Likes