I frequently am disenchanted with the new sugar the Swift project introduces. They add way too much surface area to the language by allowing too many ways to do a given thing. When you're looking at a particular line of code, it's very difficult to tell what exactly is going on without pulling up declaration sites for multiple involved things because there's too much magic.
This time around it's callAsFunction. Yeah, pretty easy to understand in their toy example. When it's deployed IRL with a ton of other things going on, and larger distances between declaration and usage, it looks like a nightmare.
I love writing Swift for myself, but I write it in a pretty boring way. When I have to work with people who get creative and clever, it's awful.
> They add way too much surface area to the language by allowing too many ways to do a given thing.
Not relevant to most situations, but I'll just add that this also makes it extremely difficult to teach a language.
I have taught Ruby -- a language with quite a sweet tooth -- for almost twenty years, and I feel like it gets worse and worse every year. Students run off to look at example code online (as they should), but discover that I have been teaching them what seems to them like a different language.
It really leaves me with no choice but to spend a lot of time in lectures saying, "You can do it this way . . . or this way . . . or this way . . . and by the way, these are all semantically equivalent."
The concept of progressive disclosure means that you shouldn't teach this kind of stuff to beginners. Stuff like callAsFunction, dynamic member lookup, etc. are for people who are writing DSLs, providing rich libraries, etc.
I wonder if there's a coding interface for beginners that works as follows:
The beginner types a declaration of "I want to _____" and the interface uses ML to auto-suggest code. The auto-suggest would be context-sensitive and fit in with previously written code. Auto-correct would work similarly. If the declaration isn't specific enough, then the auto-suggest tool would ask a series of questions that illustrate the different choices a programmer can make in achieving their goal. Finally, the tool would output code that reflects the beginner's choices.
This would increase the speed of the think-do feedback loop. Beginners would be able to jump into coding with very minimal education and "make" the application they want straight away. Of course, it would usually behoove them to eventually read some programming books and take classes, but this tool would pique their interest. The power of the auto-complete would scale back as the beginner progresses unless they get stuck. Maybe this would only work on a finite set of projects that the ML was trained on, which is fine because you can just give a lot of projects to choose from. That is until the ML becomes more powerful and generalizable.
I use Scala everyday (in spark) and generally prefer it over java but I agree that the surface area is too large. This is really evident when you start to import someone else’s libraries. The breadth of the language affords many different paths for the library writer depending On how much of a language sniper they are.
We have the exact same problem with Typescript. We've solved it with an extremely judicious use of the linter, which prohibits certain constructs, certain patterns, and tries its best to get rid of weird features. The first thing I thought when I read `callAsFunction` was "sounds like a new lint rule someone has to write to disable that garbage."
I'm not sure how invested your team is in Swift, but since we do nothing but Typescript, the investment in time (2-3 days for one the lead engineer) tweaking the linter has more than paid off ten-fold in the months since.
> We've solved it with an extremely judicious use of the linter, which prohibits certain constructs, certain patterns, and tries its best to get rid of weird features.
This is one of my concerns with adopting Typescript in my team. Would you please share more info about the kinds of things you prohibit, and why you chose them?
I work at an all typescript shop and I really recommend checking out https://github.com/xojs/eslint-config-xo-typescript#readme as this is a full time open source developer that has to field all kinds of PR's from strangers. So it saves them time if the linter flags things they're trying to avoid.
I regularly check the forums for Swift, and am dismayed at a lot of the discussion being interested in adding syntactic sugar over real new features. I fear that Swift will head down the route of C++ where there ends up being so many ways to do one thing that it's not reasonable to keep up with it all.
I was all prepared to disagree with you until I read the details ... and yeah, I agree.
I generally love syntax sugar that makes me more productive, cuts out boilerplate, and reduces cognitive overhead... but for me, callAsFunction and even maybe Key Path Expressions as Functions are going to ADD cognitive overhead.
I suppose if I only used Swift, and used it every day, they would be OK, but otherwise it's just more fiddly things that deviate from existing patterns.
How odd, I was all prepared to come in and find other folks excited about callAsFunction. It seems rather straightforward, although I will admit Swift is my daily driver.
Think most agree expressive syntax while minimizing boilerplate is at times going to require sugar. It's interesting you bring cognitive load into the question – perhaps I come at it more from an architectural light than PL fans, but that load seems largely correlated with one's purposes and patterns in using the language.
On that front, Swift has lofty and broad goals (including DSLs), but that surface area doesn't make the language worse. To take your other example, KeyPath Functions don't change anything about KeyPaths; the concept is and remains no different than anywhere else it's implemented (whether in Swift or in another language). Beginners can still use the concept, skill comes with understanding how/why it works, and mastery comes with knowing when/how to use it simply.
If Swift achieves said goals, one needn't be able to understand the last mile of any specific domain where it's used as a language. But it'll be quicker and easier than coming from another language, and the sugar will still be as sweet.
You make good points, and I think I am overreacting!
Frankly, it's misleading/silly for me to complain about Swift additions since I don't have the opportunity to write much of it, and primarily only read/consume it with Obj-C code. (I have no choice in this matter.) Basically the language already has a high cognitive load for me, so of course any additions feel burdonsome.
Also, I'm unfamiliar with using KeyPaths like that, so I'm clearly way off on my reaction to them.
The general goals of Swift make me want to learn it better and use it, honestly.
I don't think anyone in the Swift community wants the Perl's reputation as a write-only language, but changes like callAsFunction are how we get there.
This kind of magic sugar may have been spurred by the requirements for SwiftUI and its evolution, in which case, I’m all for it.
I can already imagine cases where callAsFunction() would be handy. For example, having a viewTemplate = SomeView(customization: etc) then creating copies via viewTemplate()
Why is it all about succinctness?
Swift has never been about succinctness - it’s always been about clarity, without over-verbosity. Sometimes, it just takes a decent number of characters to describe something. That’s not necessarily a flaw, especially if it makes something easier to read.
Seeing .copy() makes it much clearer to understand that what I’m receiving is a copy of something.
If you want a Button template, either extend Button with a method taking the parameters you want, or write a curried free function or something. callAsFunction adds absolutely nothing to the language, bar confusion at the call site.
`callAsFunction` is not for Swift's primary usage. It is for Python interop, used by Swift For TensorFlow. Its design is such that you should never need to know it exists unless you're designing your own Python (or other scripting) interop layer.
Then it should be some kind of python-interop module. These kinds of useless syntax hax shouldn't be introduced into the main language. >99% of swift devs will never ever "interop with python", yet they all have to pay the price of this change.
I could see its use in math-heavy code as well. That might be a rather narrow domain, but one where features like this (or operator overloading for another example) do help to reduce syntactic pain.
They've had similar thing to callAsFunction in Dart for many years now (maybe the start). I've written many thousands of lines of Dart code, and I've never used it once, and I don't like it.
You're surprised a relatively easy to implement, one off feature with no dependencies and no impact on the rest of the language can be done before a feature that impacts huge areas of the language and requires underlying runtime and compiler support?
It's easy enough to say things like "well if you don't like it don't use it". Meanwhile as a primarily swift dev I can't stop others (outside of my team) from using this disastrous syntax.
The first time I have to dig into someone's backwards codebase that uses I'm gonna flip.
Objects defining named functions allows you to reason about them logically. Car.tireSizeInches() returns an Int type. Any other developer can see that and know whats up, or type "Car." and explore relevant functionality. What the crap does Car() do? Etc.
This is a huge backwards step for swift syntax. I hope most of the large open source libs ban it.
As for implementation difficulty, see my sibling reply.
Well callAsFunction is extremely simple to implement and test. Where as async/await is a much bigger and more complex feature. It's not amazing, it's totally logical.
I understand the difference in difficulty, my point is any incremental time spent on this is wasted and could have contributed towards something else that's useful.
If I've got a new team member, I'm not going to put him on async/await. I'm going to find him something simpler to work on.
Or maybe I've been working on a feature for a few months and I need a break to clear my head so I'll work on a smaller feature for a while.
Software development isn't just assembling widgets on an assembly line. It's a creative process. Developers aren't chained to their desks pumping out a non-descript streams of "code" that become different features. Likely incremental time spent on this had almost no real impact on more difficult and useful features.
> I'm going to find him something simpler to work on.
Sure, but why should this non-feature which makes code harder to read and reason about even be built in the first place? All to save `.parse` at the call site of a parser, for example?
This syntax already exists in the language. If you want users to be able to use the obj() syntax you can already give them a closure for obj. Allowing it for user-defined types too removes an artificial syntactic distinction.
Async/await might actually help you clean up your codebase. Much better to have more syntactical sugar to keep code reviews lively and add more surprises for future developers!
i happen to be learning swift and, as a newcomer to the language, i have concluded that the level of complexity is pretty much on par with cpp. one thing i have noticed is that swift maintains readability while being complex. that's the surprised to me. complex cpp is simply not pleasant to read.
This is where a strong tech-lead and coding style guidelines are a must on any team. There will always be code ramp-up time for any engineer, junior or senior, even if everything was written in plain old C. So you need to adhere to what you consider readable as a team and discourage cleverness.
I mostly agree with you, but also can that it can be nice whenever you have a class that has some state and only a single method. If you have this, you no longer have to pick separate names for the class and the function.
It was indeed; it was proposed and implemented by engineers working on the Swift For TensorFlow project. The earlier dynamic callable features were as well, in order to offer Python interop.
> Sounds more like a problem with the team, rather than a problem with the language.
That's the same reply that people have to "C++ has too large a surface area". Granted, Swift isn't quite at C++ levels of complexity yet, but ideally, I shouldn't have to cordon off sections of the language I'm using just so my team can stay sane.
What no one seems to be able to do is make a simple language and leave it alone to focus on tools and ecosystem. It always seems to be that the momentum of solving an initial problem with the language is carried into solving more problems with the language instead of tools.
I hope we can get to a point where we don't need to build tools and ecosystems around a single language. It seems there is just an explosion of domain and generic languages and I see that as a good thing. I want more tooling compatibility so that developers can pick and choose what works for them and maybe even mix them together better. Better educational tooling so we can pick them up faster, or read languages we don't understand easier. etc, etc. things like WebAssembly, LLVM/MLIR, python->jupyter notebooks give me hope.
How do you feel about Go? It's almost twice as old as Swift, but hasn't seen the same rise in complexity, imo. Tooling and ecosystem seem pretty solid as well.
I think any useful language will have features that many people won’t use. Library writers, etc need different functionality, for example.
One thing I wish Swift had was an easy way to add more generic functionality. For example, if I want to add drop(), dropWhile(), take(), takeWhile(),... there’s no nice way to extend Array
[1,2,3].take(2)
[“a”,”b”,”c”].take(2)
Btw, I’m working a Swift Cookbook for anyone who quickly wants to come up to speed on Swift. All open source. Feel free to comment or commit.
I think you’re making my case for me. So, I’ll need a separate function for Ints, Strings, Floats... What if I have an Array of my custom Person objects?
there are holes in Swift's generics system, but this isn't one of them. you don't need separate extensions for int, string, float etc. arrays. the parent post gave you a generic implementation that works with any type of array
“Long-term storage of ArraySlice instances is discouraged. A slice holds a reference to the entire storage of a larger array, not just to the portion it presents, even after the original array’s lifetime ends. Long-term storage of a slice may therefore prolong the lifetime of elements that are no longer otherwise accessible, which can appear to be memory and object leakage.”
That one and the need for generic associatedTypes trip me up all the time.
I think about it every time I see new feature announcements like today's. The opportunity cost of the wasted effort, the new thing that will probably make things harder for everyone. Possibly including those who try to fulfill the generics manifesto.
The rest look like they aren't worth complicating the language over. Consider:
map(\.name)
Isn't really more concise than
map { $0.name }
There's one more character and the conventions for whitespace are different. Both require that you know special Swift syntax sugar features, but at least the second one is more generally useful. All we get here is a virtually identical way to do the same thing but with alternate arbitrarily different syntax.
And what's better... this?
struct Dice {
var lowerBound: Int
var upperBound: Int
func callAsFunction() -> Int {
(lowerBound...upperBound).randomElement()!
}
}
let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6()
or this?
struct Dice {
var lowerBound: Int
var upperBound: Int
func roll() -> Int {
(lowerBound...upperBound).randomElement()!
}
}
let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6.roll()
callAsFunction() makes it a little harder to relate the calling code to the implementing code -- you've got to internalize the new syntax-sugar rule -- and the calling code gets a little less clear in some cases.
Default arguments to subscript functions is probably OK. It's better if all kinds of function have the same capabilities.
callAsFunction() isn't really intended to be used in a type like Dice there. It's a bad example.
It's for Swift for TensorFlow's integration with Python, in which a bridged Python object can be called as a function. Functions are, of course, objects in Python, so that makes sense, but it would be a pain to have to call fn.call(bar) instead of fn(bar) when fn is a reference to a python object that is a function in its runtime.
It's also used in TensorFlow models, in which callAsFunction() is usually implemented as passing the model's input through all the defined layers in it.
This makes it easy to treat models as if they were functions, which they mathematically are, unlike Dice.
It's a bad example enabled by a bad language feature. Just you wait until answers on stack overflow, sample code, and open source libs are littered with these "bad examples".
Meanwhile, this example is terrible. There is no real need for callAsFunction in your example when you could have added a simple named function on the object to do so. Better yet, you could define a protocol and make sure your layer conforms to said protocol.
There is an argument to be made against features that are more likely to cause harm than good (i.e. "footguns"), but I don't think this example of using it poorly sheds any light on whether that's the case here. You could, with roughly the same level of rigor, argue that allowing people to name variables is bad by constructing an example with variables named CONFUSING, CONFUSlNG and CONFUS1NG.
Any feature, no matter how good, can be misused. Pointing that out about a specific feature isn't interesting, because it is true of every feature. And the absence of nearly any feature can be worked around, as ultimately all these features compile down to machine code, which doesn't have most features of a high-level language, so the existence of a workaround is also not a very compelling argument.
Scala has the same thing with apply() and it works great, allowing the language to have a consistant syntax for function calls, object creation, and array index access.
Closures are completely unnecessary to start with yet I don’t see you bemoan their existence.
“Callability” not being restricted to functions (closures or not) can often be convenient, especially when that use is colloquial (eg closure where symbols and collections are callable), and obviate unnecessary boilerplate.
That's not a fair comparison. You don't have to wrap every use of your method in a closure any more than you have to call the constructor of your callable class on every use.
You have to bind your state to a function exactly once in both cases.
Your example just happens to omit the constructor call required to create the callable object while you are showing the code to create the closure.
The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."
Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.
On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.
Anything you do with a closure, you can do with an object that holds the closed-over state and a function that performs the same operation on that state. For a real-world example of this in action, partial application is traditionally associated with closures, but Python's functools.partial() is implemented as a callable object that holds the partially applied function and parameters in instance variables.
> Functions are, of course, objects in Python, so that makes sense
Functions are objects in many langages. Python’s specific feature is that there are callable a beyond functions. A class is a callable and a user-defined object can be one despite neither being functions.
Then you can create a struct Die and change die to return that. By implementing callAsFunction you keep the same interface as before so existing callers don't break.
I'm pretty apprehensive about this "callAsFunction()" feature. Requiring users to name things in a specific way was the kind of awful UX our customers struggled with when using our hobbled-together marketing software.
IMO, some better alternatives, in order of personal preference:
* Give away the functionality to all types that implement only a single function.
* Require an explicit protocol conformance. I get that the language does not want features to be hidden away, but requiring a user to implement a function with a specific name and signature is literally the job of a protocol. It doesn't seem like much of a hurdle for non-beginners, who have likely been familiarized with core protocols like Sequence or Collection, to acquire a knowledge of a CallAsFunction protocol.
* Introduce a new keyword, or tag (like @implicit).
* Allow the user to unlock the functionality through a more obscure phrasing. Even something as ugly as "func ` `()" could be preferable.
There is a danger of overengineering. This is a feature that will only be used in rare situations. So it doesn't really matter that it's just a magic identifier. As long as you aren't likely to use it by accident... I'm assuming they checked GitHub that nobody has named anything callAsFunction before choosing it
I'm starting to feel Swift is being overly influenced by PL nerds. I hope they keep learners in mind - plenty of kids might be learning Swift as their first language.
It's been like that ever they set up the mailing list, what happened is that people with an abundance of time on their hands started drowning the swift team with shitty ideas. The swift team tried to resist but eventually the lunatics took charge of the asylum. Swift didn't start with a very elegant foundation because of all the ObjC baggage, but instead of simplifying the language as time goes on they have just kept adding crap to it.
> It doesn't, you can download toolchains from swift.org or build them yourselves and load them into your old version of Xcode.
IIRC, the toolchain checks the versions of SDKs shipped in the installed Xcode and refuse to compile your source code if anything's not up to date.
> But you're not allowed to submit apps to the stores with unofficial toolchains.
Not sure this is true. Apple does not allow you to copy/run Xcode from non-Apple hardware. Other than that, submitting an app built with an OSS toolchain should be fine.
What was the motivation for `callAsFunction`? It's a toy example so `Dice(...)` is clearly bad compared to `Dice.roll(...)` but what's an example where it brings clarity over the alternative?
The first example with polynomials could be achieved using closures already. Type erasure on closure contexts seems unfortunate and fixing that seems like a better long term solution (granted it's probably far harder to solve). Was excited to see talk of ML though :)
Swift on the server? Swimming against the tide? Or expressive language, ideal for writing information systems and backed by Apple with the fantastic LLVM tool chain at your disposal?
I feel like Swift is becoming more like Ruby over time. 2 of the features here (Key Path Expressions and Subscript default arguments) were recently introduced to Ruby. This isn't a bad thing, just an interesting progression.
Key path solves a real problem for me, since you can pass in a function that accepts the value to map which looks really clean, but you cannot operate on the variable unless you create a block.
I love Swift. Writing in it is awesome, the community is great, and it has traditionally been focused on a truly wonderful dev-focused experience.
That being said, releases like this are pretty frustrating because they seem to reflect a focusing on tiny bits of sugar (can we even call them that?) instead of features that have a much more positive influence experience.
Besides "better error messages" how do any of these things improve the development experience?
`map(\.name) vs map { $0.name }`. Why...?
`callAsFunction()` feels bizarre... how is `obj()` more elegant or less confusing than `obj.callAsFunction()`.
Meanwhile the Swift dev experience still has lots of annoying bugs & usability issues (though to its credit it has improved by leaps about bounds). My personal pet peeve is that sensible compiler synthesized inits[1] still have not been implemented despite requests (and proposals!) coming up again[2] and again[3] and again[4] and again[5]
Sometimes I wonder if anyone is actually asking questions like "what value is this providing to the user?". Maybe there's so many strong opinions nothing of substance can get through the approval process?
Announcements like this[6] get me excited that the language is heading in the right direction, but it'll be a lot more convincing when the a release shows that.
> That being said, releases like this are pretty frustrating because they seem to reflect a focusing on tiny bits of sugar (can we even call them that?) instead of features that have a much more positive influence experience. Besides "better error messages" how do any of these things improve the development experience?
This article is about changes to the language. The bulk of the effort in this release went into improving the _compiler_: better error messages, faster compile times, smaller code size, and fixing bugs.
And the best friends compact map example will return the names of the best friends, not the users with best friends. The article needs a bit of fine tooth QA.
I cant stand swift. Without ios apps, would anyone even touch this crap?
Add to it constant major releases requiring new xcode or MacOs. Suddenly all your existing App codes have to be upgraded to even release to appstore. I swear justice will one day be served Apple for abusing the high end smartphone monopoly.
They really lost me as a developer in the ObjC->swift change. I was trying to learn stuff and hopefully deliver as a side project, but that language change was too much in addition to the day job.
This time around it's callAsFunction. Yeah, pretty easy to understand in their toy example. When it's deployed IRL with a ton of other things going on, and larger distances between declaration and usage, it looks like a nightmare.
I love writing Swift for myself, but I write it in a pretty boring way. When I have to work with people who get creative and clever, it's awful.