Equality of functions

@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 - #84 by dabrahams
  • [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?

Iā€™ll take a pass at this sometime this week. @Joe_Groff, whatā€™s your opinion on syntax? Should anonymous structs share syntax with closures or should the syntax be distinct? If distinct, whatā€™s your thought on the double-brace syntax I have been using as a placeholder?

My opinion would be that they should share the same syntax with closures, since potential clients like SwiftUI would like to maintain their closure-based DSLs without the closure boxing.

3 Likes

Imho it would be fantastic if plain old closures would behave like callable structs ā€” at least in the debugger (e.g. captured variables can be inspected like members of a struct).
Maybe in the long run, adding some features here could even reduce the total complexity (at least for users).

@Nickolas_Pohilets I dmā€™d you a link to my draft. If it looks good to you Iā€™ll start an official pitch thread for the proposal.

I'm trying to implement a very basic prototype for swift-evolution/NNNN-closures-as-structs.md at master Ā· nickolas-pohilets/swift-evolution Ā· GitHub and I need some advice about implementation.

Looks like I need to modify constraint solver. I think I need to detect when suitable protocol is matched against the closure literal and then replace protocol type with type of its method. Alternatively I can generate a disjunction of constraints every time a protocol is matched. But that sounds needlessly expensive.

Then, when applying constraints, I need to detect that protocol was matched into a function type, and based on that perform AST re-writing.

Does it make sense?

I've started sketching the routine that would find the protocol requirement that should be implemented by the closure. How can I check if protocol requirement has a default implementation or can be synthesised by the compiler? I've found ConformanceLookupTable, but it seems to be working with nominal type conforming to that protocol. And when searching for matching protocol method, I don't have a nominal type yet.

Is it even possible in general case?

For example:

protocol P {
    associatedtype AT
    func f1(x: AT) -> AT
    func f2(x: AT) -> AT
}

extension P where AT == String {
    func f1(x: String) -> String { "" }
}

extension P where AT == Int {
    func f2(x: Int) -> Int { 0 } 
}

func use1<T: P>(x: T) {}
use1 { $0 } // Ambiguity

func use2<T: P>(x: T) where T.AT == Int {}
use2 { $0 } // It's f1

Do you think it is worth to produce constraints for all the candidates to let constraint solver disambiguate, like in the last case?

Alternatively, I can try to keep things simple and only search for requirements for callAsFunction. This still may produce multiple alternatives in constraints, if there are multiple requirements for callAsFunction, but I would not check for presence of default implementation, and let compiler verify that all requirements are satisfied after AST re-writing.

Even further simplification would be to limit implementation only for protocols with single callAsFunction requirement.