Equality of functions

Discussion seems to get stuck a bit.

@Joe_Groff, @anandabits, any comments regarding type(of:) behaviour?

I am not and expert on the runtime implementation so I don’t have an informed opinion on your questions about runtime representation. That said, I do agree with your analysis of the expected behavior as a user.

I would expect it to return some anonymous type. We probably shouldn't make any guarantees about what kind of type it produces.

I was also thinking that a more incremental approach to this sort of thing might be, instead of introducing functions as a whole new kind of constraint, would be to allow the use of closure literals with protocols that look like function constraints, something like how Java lambdas work. If you have a protocol with a callAsFunction requirement, like:

protocol CallableWithInt {
  func callAsFunction(_: Int)
}

then, in a context that has a CallableWithInt constraint, we could allow you to use a closure literal as a value of an anonymous type conforming to the protocol, while also allowing synthesis of Equatable/Hashable/Comparable/Codable if the closure context values conform.

2 Likes

I like this idea! It's related to a more general idea I have been thinking about for providing syntactic sugar for anonymous structs. The idea is to use double braces {{ }} to denote an anonymous struct, a capture list to initialize properties, and support trailing anonymous types similar to how we support trailing closures.

When the protocol only has a single method (or possibly subscript) requirement, the body could be used to fulfill the requirement:

protocol Monoid {
    associatedtype Value
    var empty: Value
    func combine(_ lhs: Value, _ rhs: Value) -> Value
}
extension Sequence {
    func fold<M: Monoid>(_ monoid: M) -> Element
       where M.Value == Element
}
[1, 2, 3].fold {{ [empty = 0] in $0 + $1 }}

Ideally this could be made to work in cases such as Monoid & Hashable where memberwise-synthesizable conformances are assumed and the compiler understands that the single requirement being fulfilled is combine.

In cases where multiple requirements must be implemented manually, the body could also support explicit declarations:

[1, 2, 3].fold {{
   typealias Value = Int
   var empty: Int { 0 }
   func combine(_ lhs: Int, _ rhs: Int) -> Int { 
       lhs + hrs
   }
}}

To provide a concrete real world example applied to SwiftUI:

NavigationLink("Details") {{
    VStack {
        // details view content
    }
}}

I haven't thought much about whether that exact syntax is viable or not, but I think sugar along these lines is very interesting. Declaring a nominal type can be sometimes be inconvenient, especially considering the restriction on declaring them inside a function in generic contexts.

For my use cases having function types as structural and not requiring explicit naming is quite useful. Allowing only nominal function protocols to equatable would definitely decrease value of the feature, but we could live with that.

Also, I need support for generic function, which in this context, implies generic protocols, which Swift currently does not have. In combination with generic type alias syntax mentioned in Improving the UI of generics, this may work, but it adds even more boilerplate code:

typealias CollectionOf<T> = Collection where Self.Element == T

protocol Predicate: Hashable {
    associatedtype Value
    func callAsFunction(_ value: Value) -> Bool
}
typealias PredicateOf<T> = Predicate where Self.Value == T

I've been thinking how exactly would I handle the need for declaring function types as nominal types. And assuming generic type aliases with constraints are available, I would probably just do this:

protocol Function0Protocol {
    associatedtype Result
    func callAsFunction() -> Result
}
typealias Function0<R> = Function0Protocol where Self.Result == R

protocol Function1Protocol {
    associatedtype Result
    associatedtype Arg0
    func callAsFunction(_ x0: Arg0) -> Result
}
typealias Function1<A0, R> = Function1Protocol where Self.Arg0 == A0, Self.Result == R

protocol Function2Protocol {
    associatedtype Result
    associatedtype Arg0
    associatedtype Arg1
    func callAsFunction(_ x0: Arg0, x1: Arg1) -> Result
}
typealias Function2<A0, A1, R> = Function2Protocol where Self.Arg0 == A0, Self.Arg1 == A1, Self.Result == R

...

Generic protocols are unnecessary. A generic function could be modeled as a protocol with a generic callAsFunction requirement, which would in fact be something our structural function types don’t support today which protocols could.

If I understood you correctly, we are talking about two different things:

For the type (T) -> Bool, I'm talking about the case of:

struct Foo<T> {
    var f: (T) -> Bool
}

While you are talking about the case of:

struct Foo {
    var f: <T>(T) -> Bool
}

Generic function type != Type of the generic function

@anandabits, regarding the Monoid example.

When type is Monoid & Hashable, combine is not the single requirement anymore - there is also a hash(into:). Even if static func == is not considered as because it is static, somehow remaining two should be disambiguated. There should be a way to tell compiler which method block implements.

This can be achieved using annotations, but IMO the simplest approach would be to require function name to be callAsFunction.

Should we also require that function to be non-mutating?

Yes, but:

I don’t think annotations are necessary. Anonymous structs would be syntactic sugar. In cases where a user needs fine-grained control they are free to declare a nominal type.

I’m not sure what you mean by requiring the function name to be callAsFunction. If by that you mean the name of the protocol requirement the body of the anonymous struct is fulfilling, I disagree. Part of the point here is to provide syntactic sugar for protocols that have otherwise well-named requirements. It would not be desirable to have every protocols start naming requirements callAsFunction just to be compatible with syntactic sugar, and without doing that they would not be eligible for the sugar under your proposed rule (if I understand it correctly).

No! The anonymous struct should be free to mutate its captured values (properties) if the fulfilled requirement is mutating. The idea is that this is just syntactic sugar for things you can already write manually by declaring a nominal struct and passing it an instance of it.

I’ve run into quite a few places where this would be very convenient. In addition to the example I gave above, when we have constrained opaque return types this would be a very convenient way to implement operators in libraries where each operator returns a small type specific to the operator (i.e. Map, `FlatMap, etc structs). Once we have constrained opaque return types we could express all of the relevant type information without needing to introduce a nominal type.

I got your point regarding the naming, makes sense to me.

Could you elaborate more about the mutating methods? Would mutable variables be the variables captured from the context or something else? In general, can such sugared structs capture mutable variables from the context? Would mutations be shared with the context? Probably yes, otherwise difference from regular functions is confusing. But struct fields need a type, current allocations with heap local variable metadata would not work. How would they be boxed then?

The capture list would turn into property declarations, initialized in a memberwise fashion with the values that were captured.

Captures would be by value, so sharing would only happen when the captured value has reference semantics.

Boxing would not necessarily happen. From the compiler’s point of view the instance of the anonymous struct that gets passed would be no different than an instance of a nominal struct. Depending on how the optimizer handles the code the value may or may not get boxed.

I realize I didn’t share a complete example of what the de-sugaring might look like. I think that might help explain the idea. So, the Monoid example above would de-sugar as follows:

[1, 2, 3].fold {{ [empty = 0] in $0 + $1 }}

becomes

struct SomeNameUsedByTheCompiler: Monoid {
    let empty = 0
    func combine(_ lhs: Int, _ rhs: Int) -> Value { lhs + rhs }
}
[1, 2, 3].fold(SomeNameUsedByTheCompiler())

And the SwiftUI example above would de-sugar as follows:

NavigationLink("Details") {{
    VStack {
        // details view content
    }
}}

becomes:

struct SomeNameUsedByTheCompiler: View {
    var body: View { 
        // details content view
    }
}
NavigationLink(“Details”, destination: SomeNameUsedByTheCompiler())

So here’s a final example with a mutating method requirement:

protocol Foo {
    associatedtype Bar
    mutating func update(with value: Bar)
}
aFunctionTakingFoo {{ [var count = 0] (value: Int) in 
    count += value
}

De-sugaring to:

struct SomeNameUsedByTheCompiler: Foo {
    var count = 0
    mutating func update(with value: Int) {
        count += value
    }
}
aFunctionTakingFoo(SomeNameUsedByTheCompiler())

Does this help explain how the de-sugaring would work?

IMO, the explicit name and boilerplate required for a single-use / ad-hoc nominal struct are significant enough to be annoying. This is especially the case when lexical distance is necessary (i.e. a local struct is not possible in the current lexical context). These syntactic issues can unnecessarily complicate code that should be simple and can discourage some library design choices that would otherwise be viable.

There is a bit of misunderstanding here, I was speaking about boxing of local variables:

var k = 0 // boxed and stored on heap
let f = { k += 1 }

But, since sugared structs can capture only by value, that’s not applicable here.

Anyway, I’ve understood your answer, let’s move on.

@anandabits, what is the motivation for double curly brackets syntax? Why not single ones? Is it causing any issues in lezer/parser/type checker or just to make it clear for the developers that those are not regular functions?

To be honest, I haven't looked at potential issues here in detail. So I'm not entirely sure whether the double brace syntax is viable.

Anonymous structs are quite a bit different from closures. I think it is important that the syntax be different in some way in order to make this clear. It should also be very lightweight as this is a syntactic sugar feature. Double braces are a mostly arbitrary strawman syntax.

If this feature is ever seriously considered I'm sure there will be ample bikeshedding on the syntax details. The community loves to debate this kind of thing.

I'm considering this pretty seriously and aiming at making an evolution proposal.

But to truly solve my use case, I need all of these:

  • Allow PATs as existential types - Lifting the "Self or associated type" constraint on existentials
  • [Nice to have] Provide syntax for opening existential types - you can already use Self in protocol extensions, which can be used to implement opening.
  • Provide syntax for disambiguating protocols as constraints vs protocols as existential types in extensions, to be able to conform any Hashable: Hashable
  • [?] Does that conformance above imply any (Hashable & Foo): Hashable or extra work needs to be done?
  • Clarifying existential types to be able to use PATs as generic protocols.
  • Enable local structs declared inside generic functions - Could some types be allowed to be nested in generic functions?
  • Allow block syntax as a syntax sugar for anonymous structs.

First five are already part of roadmap sketched in Improving the UI of generics. Any advices on how can I contribute to that?

For the last two I'm gonna make evolution proposals. Assuming they are approved, is it expected from the author of the evolution proposal to implement it as well, or implementation is handled by the Core team?

Any rough estimate on how much would it take to implement all this up to releasing as part of stable Xcode release?

Proposals must be accompanied by a prototype implementation before a review takes place. If you want to move those final two bullets toward adoption, I would recommend coming up with a potential design and workshopping it on these forums (in Evolution > Pitches) with the community. This will help head off any design issues before you do the work of actually implementing the feature, and/or help identify any enthusiastic community members who could assist with an implementation if you don't have the time/resources to prototype it yourself.

These would certainly make the feature more useful, but I think they're separable concerns. Something like protocol BinaryOperator { func callAsFunction(_: Int, _: Int) -> Int } could already be used as an existential type without other language changes. Furthermore, much of the benefit of treating a closure as an anonymous struct comes from being able to make types generic on that struct and avoid the boxing altogether, which would skip over the use of existentials entirely.

I would say that this is just an implementation limitation that could be lifted, and not a new language feature per se. There's no fundamental reason types can't be declared in generic function contexts, and most of the historic implementation limitations have been addressed.

I think you'd be able to focus your energy on primarily this part:

I agree. The use cases for anonymous struct syntax are broadly independent of existential. In all the use cases I've seen for this syntax, if type erasure happens its in the implementation of a library. The current workarounds are good enough. There are certainly user-facing use cases for generalized existentials but I haven't seen any that would also use anonymous struct syntax.

This is great to hear! I hope it happens relatively soon.

I'm not able to help on the implementation side, but if you're able to work on implementation and would like help writing a proposal I'd be happy to do that.

I'm not able to help on the implementation side, but if you're able to work on implementation and would like help writing a proposal I'd be happy to do that.

@anandabits, jump in - https://github.com/nickolas-pohilets/swift-evolution/blob/master/proposals/NNNN-closures-as-structs.md

Any suggestions of the existing cases of AST transformations in the compiler that I can use as a reference? Function builders is an interesting one. Probably looking into how compiler synthesises conformance for Equatable/Hashable/Codable would be helpful. Anything else?

Terms of Service

Privacy Policy

Cookie Policy