The example I've run into in my own projects where it would be useful to have a stateful type that also represents a "thing that can be evaluated" in the mathematical sense. For example, a domain<->codomain mapping that is built with a Range of domain values and a Range of codomain values, and applying that mapping to a domain value yields a codomain value. let y = mapping(x) for some protocol or value type mapping is cleaner than an operator based syntax, because the function call syntax looks just like the mathematical concept it's imitating.
The approaches I've used before have been either an explicit apply member function, which loses some of the succinctness that can be useful in those contexts, or using subscript, which feels like an abuse of the square brackets. The proposed call here would serve the purpose nicely.
I can see other mathematical concepts being modeled in the same way, where you don't want to explicitly have to distinguish the conceptual model from the "function" that applies it. Stateful random number generators come to mind as another possible use case here.
I don't find "make it look like math" a particularly desirable path for a general purpose language to follow. I suppose I can see why those who spend their time in that domain might want it, but to anyone else, something named like mapping.codomainValue(for: x) seems strictly better. It's been said that we don't want to produce dialects of Swift. I know that's normally meant in the compiler flag sense, but adding syntax like this seems to encourage variants of Swift code that no one but the original author can read. It's trading compiler flags that can turn off required function names for building it into the language itself, which seems like a distinction without a difference in its impact to the community.
Parsers, memoizers, and other types that represent the operation to be done to the input, really. That's why bound closure is solid and why math comes up.
Sure, but just about any syntax or pattern can be abused by developers who use it in a manner that isn't intended. C++ has had the ability to overload operator() for at least a couple decades and that community doesn't suffer from widespread abuse of the feature. Indeed, the ability to have stateful types that can be applied to values treated identically to functions is very useful in a number of important design patterns.
I will grant that the fact that Swift has context-capturing first-class functions means that many of the uses of operator() from early C++ (until lambdas were introduced) can be duplicated that way instead, but there's still value in being able to treat state and application as a single unit. Today in Swift, you only have three options:
You pass the state and call an explicit function, losing the notational benefits that you want
You pass the state's member function, losing the ability to query or mutate the state itself because it's captured by the function and inaccessible otherwise
You pass both, obfuscating the API
A call operation resolves this nicely for the problem domains that benefit from it.
I suppose my uncomfortableness here comes from a lack of a guiding principle that this proposal addresses. There's nothing in Swift's design that says this is an obvious feature, nor in any official documentation I'm aware of. It just seems like a standalone feature ported from other languages that solves a largely esoteric ergonomic issue. It's not something that can be built on to enable other features or something which makes other features cleaner. It just... is.
Does thinking of this analogously to subscripts help at all? Or do you think that subscript notation is similarly superfluous and collections should have instead just provided an elementAt(_:index) method or similar. To me this seems like the natural way to work with types that are function-like, just like a subscript is the natural way to work with types that are collection-like.
MHO is that a call keyword make the code more clear (and clarity is what we generally optimize for). That said, MHO isn't really very pertinent here, it would be great for the proposal to capture all of these suggestions in the alternatives considered section so the community can properly consider the different options and weigh them.
I'm with @Jon_Shier here; the examples don't do a particularly good job of showing why we need this feature. I suppose there's already precedent with @dynamicCallable, but without that precedent I'd be quite negative on this. Speaking personally, every time I've tried to do something clever like this with subscripts or operators it ends up being messy and I've gone back to just calling functions.
For the motivating example in the proposal text:
return dense(flatten(maxPool(conv(input))))
Something like that reads much clearer to me if it's written as:
With that said, if the consensus is that static callables should be in the language, a call keyword akin to subscript as proposed seems the right way to go.
Thanks. I agree those examples are not super convincing. I'll include other motivating examples like what Joe Groff suggested. I think @allevato has some really great points about "treating state and application as a single unit" so I will incorporate those too.
There will be a delay in updating the writeup. Since most feedback I've gotten is positive, we will go ahead and focus on the implementation for now.
Would call even need to be a disallowed-as-identifier keyword? It could only appear directly inside a type/extension, where no normal code can appear, right? Wouldn't that make it a good candidate for being a contextual keyword?
The list of keywords has subscript, to which I think the same should apply, in the list of normal keywords. But is that actually necessary?
I believe call-as-an-identifier should still compile, except that there will be a warning and a fixit.
In places only declarations are accepted, call should be parsed as a declaration. This covers type declarations (including nested ones) and extensions. In places where statements are accepted, call should be parsed as an expression.
That looks reasonable, and pretty much like what I'd expect. But is there a need for the warning and fixit (which wouldn't be source-breaking, but still a little annoying churn) at that point?
Asking the other way around, is this:
struct T {
func call(arg: Lol) { ... }
}
worse, less readable, more likely to lead to bugs, ... than this:
struct T {
func `call`(arg: Lol) { ... }
}
(I'm assuming that is what the warning/fixit would suggest)
I agree with you. I think the main concern is that it's inconsistent with other keyword function-like constructs like init and subscript, but call as a declaration name is a lot more common than init and subscript.
FWIW, if we allowed call as a declaration name, language documentation would need to add a special-case:
Keywords cannot be used as declaration names directly, except call.
Wouldn't it just go in the list of "Keywords reserved in particular contexts", that can already be used as declaration names? Imo it would be reasonable to make init and subscript behave like that as well.
is subscript a verb or a noun or both or some other option that I haven't considered?
Would something like invocation work, is my question, really. I recognize that it is longer (less convenient) and a little less used, but both of those things could make it less costly as a keyword.
I also think that having call act as a contextual keyword would be the best way to go. If we want to maintain consistency with subscript, we could make that a contextual keyword as well.
subscript as a keyword is intended to be a noun (the same way func(tion) and var(iable) are nouns). But then again, let isn't really a noun, and init and deinit are abbreviated to the point where it's not obvious that they're supposed to be nouns rather than verbs.
@dan-zheng and I did some major edits to the proposal. We feel that it is ready for a formal review. The PR is here: apple/swift-evolution#1009. Please take a look and let us know if there's any concerns or possible improvements.
In the updated proposal, we incorporated feedback from this thread and expanded the source compatibility section (treating call as a keyword only when parsing nominal type members), as well as alternative designs and motivation. Thank you to thread participants so far!