I don't want to rude or fight with a more knowledgeable person, but strictly speaking current closures in Swift are still reference types with value semantics, no? We can view them as values because of the guaranteed immutability, but they still are 'reference' types. That's what I learned so far, why would we deviate now?
Their reference-ness is entirely an implementation detail, unlike classes, since the context has no observable identity. Identity and shared mutability are the two primary semantic properties of reference types, and neither of them apply to closure contexts.
I'm surprised to see this because I've always seen closures referred to as reference types since the first release of Swift, as they are in The Swift Programming Language.
These are certainly important. But as I have said upthread, tuples do not support ad-hoc context-specific conformances. And as we have already discussed, it would be unfortunate to restrict this sugar to adding a single function to member wised synthesized conformances, especially if that function had to be given a specific name.
SwiftUI view modifiers are a good example of the kind of use case I think is well-suited to this sugar. Let's look at an example from this blog post https://netsplit.com/swiftui/view-modifiers/:
struct Card : ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.white)
.cornerRadius(8)
.shadow(radius: 4)
}
}
extension View {
func card() -> some View {
modifier(Card())
}
}
Here we have a struct with no real purpose other than to be the result of an extension method. I'd like to be able to write that as:
extension View {
func card() -> some View {
modifier {
$0.padding()
.background(Color.white)
.cornerRadius(8)
.shadow(radius: 4)
}
}
}
This wouldn't quite work in SwiftUI as it is currently designed because modifier
doesn't actually constrain its modifier
argument to ViewModifier
. I'm not sure why that's the case and perhaps it could change. I can't imagine a use case for calling modifier
without that constraint being met.
Regardless of whether or not SwiftUI would change, this design pattern of pairing an extension method with a small wrapper type is common in Swift. This proposal supports this pattern well in cases where an explicit nominal type isn't necessary and the wrappers are small. This will be even more the case when we have constrained opaque result types.
This is clearly a use case that is not going to be met by tuples. It is also clearly a use case where we do not want the fulfilled requirement to be called callAsFunction
.
Sure, the main reason ViewModifier
is a protocol rather than a (Content) -> Body
closure is for unboxing. It basically is a closure over a function otherwise, which fits the intent of this language feature fairly well IMO.
Maybe it's more precise to say that closures can introduce implicit boxes when they capture a mutable variable. But the fact that they can introduce shared mutability (i.e. reference semantics) implicitly is important. I think this is why they have been called reference types. The contrast with this proposal is that the box would need to be introduced explicitly by user code if desired. That's not a trivial difference and I think it's an important one, especially when the expectation is that user-code will not introduce reference semantics when passing a function / conformance to a library.
There isn't a fundamental reason this feature has to behave differently from closure values. Something like this:
var x = 1
foo { x += 1 }
can synthesize a context type that looks like struct { var x: Box<Int> }
, capturing a reference to the variable, and then the body method would have the same semantics as a closure invocation function, but without the boxing, and potentially conforming to some protocols.
This pitch is very refreshing, thank you very much!
It is also very broad, surely too much, so I'd like to add a few ideas that may help focusing it.
My thoughts exactly.
I have another one: easily feeding protocols that "export" a unique value (they have one unique property, or one function without argument). This is the bell this pitch immediately rang in me:
// Given
protocol FooProvider {
func makeFoo() -> Foo
}
func eat(fooProvider: FooProvider) { ... }
protocol StringProvider {
var bar: String { get }
}
func eat(stringProvider: StringProvider) { ... }
// Let's just provide equivalent closures
eat { Foo("😍") }
eat { "🥰" }
This could greatly help some injection strategies and remove the need for ad-hoc types.
Keep in mind that ViewModifer
is just an example here. Sometimes the protocol in question isn't quite as simple as a single function, while still adopting the extension method plus type design pattern and being simple enough than an inline, ad-hoc conformance is warranted. There may be additional requirements fulfilled by stored properties, default implementations, or even a couple of explicitly specified implementations provided using the in struct
body form. These use cases still benefit from avoiding the ceremony and indirection associated with a fully declared local type.
Sure, anything is possible. But that isn't what I would personally want to see happen. I would strongly prefer to see simpler sugar over a manually written struct, with any reference semantics being introduced explicitly by the user.
Thank you for the kind words, and you're welcome!
Can you elaborate specifically on what aspects you think are too broad?
I'm not against the idea of allowing captures to be used to satisfy additional requirements of the context protocol, though I think that's nice-to-have relative to the core functionality of being able to close over unboxed context. It could be added later, and I worry that it stretches the proposal too far if you don't focus on that core capability first. I think ViewModifier
is a pretty good example to go with, really, because it's an API that likely would've been designed much differently if we had the closure unboxing capability already.
Is there a reason beyond implementation effort that you feel this way? IMO, it would be reasonable to break it up if implementation necessitates that. But if not, I'd prefer not to artificially narrow the scope. I have use cases for this beyond the single explicitly satisfied requirement use case and am eager to use it.
Thanks, I'm going to include this in the next draft.
"Anonymous Structs" already exist as tuples, to paraphrase @JohnEstropia.
Please don't take what I said out of context. I was saying most people mistake this feature as something tuples already do because of the name "Anonymous Structs". I very much support this proposal and have actually provided real-world use-cases in this thread. Again the important point here is the inline conformance to protocols ("ad-hoc conformance" as Matthew calls it).
So it took a bit of time to wrap my head around this proposal, and I have a couple reactions:
-
I can see the value in the overall goal of light weight ad-hoc protocol conformances. I actually ran across a case in my own code where this would have been useful after reading this thread last night.
-
I agree with what others have said that it seems like it would be better to have more syntactic differentiation from closures. I'm imagining a world where a trailing "closure" could be either a traditional closure, a function builder, or an anonymous struct, and I can imagine this would make it much harder to reason locally about what's going on in unfamiliar code. Also I am not an expert, but it seems likely that with such similar syntax, this could easily have an impact on compiler performance.
-
The treatment of single-requirement bodies strikes me as too-clever-by-half. I can understand the appeal of writing incredibly terse code with flexible semantics, but I worry about trying to read unfamiliar code like this (see point 2) and also I worry about debugging code like this. Also I can potentially see this leading to highly atomized protocol design, where codebases might artificially end up with a ton of single-requirement protocols just so they can be used in this way.
I agree with @Joe_Groff that this might be a "branding issue", and this problem would be highly mitigated by narrowing the scope of this implicit behavior to callAsFunction
requirements. Thinking about this feature almost as "closures conforming to protocols" makes the current proposed syntax seem more natural to me than thinking of it as anonymous structs.
Overall I think it's a very interesting proposal, but there's a lot here and I wonder if it might be possible to approach it more incrementally and start with a more explicit, less sugared syntax and spend some time with it in the wild before deciding on exactly this completed package.
edit: one other thing - I'm not a big fan of the implicit "let". I'm not aware of any other place in swift where a variable can be declared without "var" and "let", and I'm not sure what the argument would be to break that convention here.
The pitch sounds like it wants to cover 100% of struct building, without structs. This makes the pitch big, probably too big for a single proposal. I'm sure most of the ideas that are developed in this pitch could be moved to the "Future Directions". This could help the pitch focus an a few more targeted benefits. "Anonymous Structs" could just become an implementation detail instead of the main topic.
Actually this is backwards. Closure captures are always immutable and never require an annotation. Supporting the var
modifier is what makes this proposal different from function closures.
Closure parameters, and tuple members, both of which the proposed syntax is supposed to be suggestive of.
What exactly do you mean by 'unboxing' and could you maybe explain why this proposal is necessary for it? If there is a more favourable representation for a closure, why isn't the compile able to exploit that right now?
Equatable for closures is cool, but I'm not sure how useful it would be in practice given the Self
type constraint and limitations that come with opaque types. As I understand it, each anonymous struct would be a different type, so you would only be able to test a closure with itself (essentially only checking its captures for equality).
As for the SwiftUI/wrapper case, I think this would be much better solved by allowing nested types in generic functions.
It's analogous to the comparison between a generic function and one that takes an existential type; given a value of protocol type P
, in the general case, the compiler has to deal with a value of potentially any conforming type since the specific conforming type has been erased, whereas given a generic argument <T: P>
, the type information is preserved, and you can pass it through into other generic types, return values of the same type, and so on, without needing indirection when the function can be specialized. There are of course opportunities where we can eliminate the indirection and specialize through existentials, but it's much harder to do so in the general case. Closures are effectively always existential types, and this proposal is a way of providing the analogous generic argument feature.
Yes, so - we have an ExistentialSpecializer
optimisation, which makes generic versions of functions which take an existential and specialises them. I figured we could do the same with closures even without this feature.
I mean, I figure that to effectively optimise an anonymous struct with a callAsFunction
member in place of a closure would require pretty-much the same machinery as that optimisation. The compiler could be doing that today and we'd never know.
I'm still not exactly getting why we need this exposed in the language in order to make those optimisations. I'm of the opinion that if we add syntax to unlock optimisations, that syntax should be telling the compiler something which it couldn't otherwise infer. I don't get what extra information is this syntax telling the compiler that it couldn't get from an ordinary closure.
We could, but there are limitations to when we can do that. It's also less likely to help with types that compose generic fields; a struct Foo { var x, y: P }
is more than likely going to box x
and y
when it can't be exploded and the two existentials individually specialized, whereas a struct Bar<T: P, U: P> { var x: T, y: U }
will never box them because it inherently directly contains the two fields. Closures are in the same boat; SwiftUI and even the standard library collection wrappers for .lazy.filter
and .lazy.map
would benefit from being able to embed their closures' contexts directly instead of via a box.
Though it seems like a cool and somewhat applicable feature to have the special syntax for single-requirement bodies, it seems a bit like magic IMO from the side of the reader of the code. I feel like it would be helpful, for the sake of clarity and readability, to indicate what the single requirement's identity is (maybe something like function signature) at least.