Is Swift 6 a good first language?

You can still have top-level code, or paste expressions into a REPL, or whatever, where you do this:

let game = Game()
game.spawnInvader()
// …

But you can do this without directly accessing that global variable elsewhere in the program.

We don’t teach line numbers and GOTO anymore and instead start with structured control flow and subroutines as fundamental concepts. Similarly there’s no reason to teach bad practices involving global variables.

2 Likes

Nobody suggested that. I said, assuming value semantics, nesting variables in other types doesn't buy you anything. Resorting to reference semantics just proves that point.

@svanimpe - I'd be interested to see how you get on. I don't think it matters especially whether it's kids or just new programmers. If it's a good first language for adults, we can assume it's a good first language for anyone ready for their first language.

A couple of people have at least suggested that new programmers should just deal with concurrency. After all, modern computers have more than one core. There may be some truth to that, but Swift Concurrency is too advanced for your first program (and does come up almost immediately in Swift 6).

Some kind of single-threaded option for simple programs, especially Playgrounds, seems like a better direction.

3 Likes

i’d like to add that, while top-level code could be considered beginner-friendly, “beginners” are not the only people who might have a use for top-level code. i myself have at times felt frustrated that single-file Swift scripts with top-level code don’t just do the Right Thing by default.

5 Likes

I for one would love it if declarations in top level code (main.swift) defaulted to fileprivate. Even if we had to put such a change behind a feature flag.

2 Likes

If you follow this line of reasoning to the end, it leads us right back to Java, where beginners are forced to deal with all the ceremony of “best practice” just to get to “Hello, world!”.

And for what it’s worth, global variables are not a “bad practice” in and of themselves. Using global variables instead of function arguments is a bad practice because it makes code non-reentrant and hard to follow. And using global mutable variables in concurrent programs is a bad practice because it makes it far too difficult to maintain correct program state.

You can teach a beginner the first rule once they’ve learned about functions, long before you get close to introducing data structures. Concurrency requires so many more foundational topics, yet we’re forcing beginners (and scripters) to reckon with it before anything else.

12 Likes

You don’t need to deal with concurrency right away. And Swift Concurrency isn’t advanced until you dive-in: you are biased towards its complexity because you have much more knowledge about language and various concepts, for a new person to programming there won’t be that hard to deal, compiler will just give the message “that’s wrong”. If you want to deal with concurrency at the start, Swift is actually much nicer to you, rather then creation of low-level primitives on your own and managing them. You can write totally OK program not understanding concurrency in depth, but only relying on compiler errors.

Apart from static/global variables, what’s else that makes concurrency to pop-up? I’ve been writing a few CLI utils for the past two month using Swift 6 and had no need to deal with concurrency, getting zero errors from compiler about that, so I can’t imagine what can be in a beginner’s code that will lead to a concurrency?

3 Likes

I agree that static property count is not concurrency-safe because it is non-isolated global shared mutable state is a very daunting message if you're just learning the basics of programming†. On the other hand, I think it's great that even at the very beginning Swift encourages avoiding dangerous constructs like relying on global variables.

Let's say this hypothetical programmer could use some sort of main-actor-only mode for the code which would remove any errors about global mutable state. It's not far-fetched to think that at some point this programmer may actually want to use some sort of basic concurrency. At that point, disabling this "main-actor-only" mode would likely trigger dozens of errors.

Worse, even: I think the most likely approach for a programmer that has been relying on static/global variables for their code wouldn't be to refactor their code in a sensible way like @Slava_Pestov suggested, but rather to try to make the existing approach (with static/global variables) work with Swift concurrency. Most likely, by adding tons of @MainActor annotations and unnecessary suspension points (to navigate around the isolation context switching).
I don't think many new programmers at this stage would realize that the many concurrency errors they'd be getting constantly would be rooted in the choice of static for global mutable state: most likely, they'd blindly apply Xcode's "fix" of using @MainActor, and then attempt to deal with the more nuanced errors that would happen downstream as a consequence.

IMHO having the diagnostic encourages the right behavior, but it's too intimidating for a newcomer. Instead of removing these sort of diagnostics by relying in a main-actor-only mode, I think these would be great places to point to more in-depth documentation (educational notes?) that slowly introduce concurrency as a topic.


†TBH, I think by the time a programmer is reaching for static in their code, their grasp of programming concepts like concurrency or multi-threaded code may not be as loose as we're assuming in this thread.

6 Likes

@vns - If you can create command line applications without really needing to deal with concurrency, that suggests things are not as bad as I thought. I've been mostly playing around with Metal and SwiftUI (where the basic boilerplate is concurrent), so that may have tainted my opinion.

It'll be interesting to see how @svanimpe gets on when they update their course for Swift 6.

1 Like

I have made very good experiences using Swift to teach people programming in general as well as in the context of app development (i.e. Apple platform applications). My audience, however, usually consists of (adult) trainees and/or university students, I have not worked much with kids.

A point I would explicitly add to the good list of @tommyming would be Swift's "local reasoning" principle: It is a lot easier, in my experience, if learners can focus on a limited number of lines in one file when trying to figure out what's going on.

Concurrency, by necessity, subverts this local reasoning to an extent. While the "structured" part in Swift's paradigm mitigates this somewhat, we are fundamentally teaching "what happens if two different things have to occur concurrently" when we have to introduce it to any newcomer.
So IMO it is kind of understandable that there's friction, and things like static global variables have to be explained in more detail than is probably desirable.

How much of a problem this becomes during teaching depends on the circumstances:

  1. How soon will the students run into problems with this?
  2. Are they mostly learning by themselves, relying on compiler errors and warnings, or do they have a teacher present?

Naturally point 2 is easier to address: Indeed, if learning on their own, the current state of things makes it hard for them. Unfortunately I don't know exactly what we could do here other than maybe tweak documentation and compiler fix-its, etc.
If they can ask a teacher, I think it is okay to provide a fix for now and delay a more detailed explanation ("We will use instance variables for now and learn why later"). I believe in Swift such a delayed explanation would be relatively rare (generics would be another one, although it's less likely the student runs into them "on accident"), so there's not too great a risk of "getting lost in future outlooks".

Point 1 is something that seems to not be addressed in this thread so far.
To me it reads as if most people assume a "classic" intro into programming: Write very simple code like "Hello World" and such, which implies more or less linear, non-looped command line programs that output things to the console. In my experience this is good if you want to focus on the underlying principles like "How does a program actually compute things?", "What is an algorithm?", and pieces of data structures.
As the examples show, I think this also introduces a high chance students would write code that triggers concurrency warnings (thus exposing them to the concept relatively early).

However, I've also come to learn that this approach when introducing programming can be demotivating for many students, especially if they're not already "STEM hyped". They often want to see how "real apps" are made, i.e. hope that when writing some code and pressing a button, the result will be a window that pops up and can be interacted with.

The SwiftUI tutorial on the Apple pages address this by immediately jumping into concrete UI development. The resulting "Landscapes" app does provide a pretty quick success for students and I have seen them get hyped for more, which makes explaining "regular, boring code" then easier.
Of course there's a downside to this: SwiftUI itself is, like concurrency, relying on high-level concepts like result builders, opaque result types (and thus protocols), and a flat-out black-box in terms of how "those weird structs" manifest into actual graphics on the screen. It does, however, deliver a decent introduction that illustrates a complex program without delving into concurrency [1].

In the past, before Swift and SwiftUI I have once worked with a UIKit app template and tried a kind of "hybrid" approach: Students could see a "regular UI" and I did actually start with "how to add a button", but then connected it to a place in code that allowed them to execute simple code as if they would work in a playground. It's been a while, though and I don't know if I still have the template...

[1]

At least the last time I checked it...

11 Likes

It does seem like a good teacher could navigate their students around concurrency effectively, but it would require some hand-holding, which self-taught beginners can't rely on.

If I'm honest, Swift has never been a "good" language to teach someone programming. It's not a "bad" language but there are many languages out there which are far better first languages (though all have their flaws). I'd probably call Swift an "OK" first language (i.e. usable in the hands of a great teacher).

Swift, unfortunately, has a LOT of complexity. And while some may bring out the theory of "progressive disclosure" to try and counter that complexity, in practice Swift thrusts that complexity onto you from day one… ok, maybe day two.

Concurrency is definitely one of those aspects of complexity. Ironically it is one you can progressively disclose by not using the Swift 6 language mode (something that sounds like it will be supported indefinitely) but then you'll need to start teaching kids about language modes and how to turn it off (if you even can in something like Swift Playgrounds).

I think if Concurrency can't be turned off in future then absolutely I think Swift becomes a "poor" language for learning to code. While it may be a shiny new feature representing a big change to the language, the reality is that concurrency is (and always will be) an optimisation edge case for the vast majority of apps, and as such most kids learning to code likely won't need to know about it for years.

The other irony around Concurrency is that GCD is arguably easier to teach and far more in-line with the goals of "progressive disclosure". Once you know more about both systems you do see the benefits of the Swift Concurrency approach over GCD and that they, on balance, outweigh the cons, but that requires a lot more technical understanding than you want to give a kid (and any knowledge of concurrent programming is already pushing it)

6 Likes

This is true, but as I understood your original post, you'd be there to do that hand-holding, right?
In the general case, of course, this is not possible by definition, but to be fair, these kinds of trip wires exist in any kind of learning and it is impossible to foresee them all.

An example I recently encountered: Why use let, i.e. constants, when var works just fine and allows you to "forget" one keyword? It seems obvious to us and surely, if you give a written explanation, just as TSPL does:

If a stored value in your code won’t change, always declare it as a constant with the let keyword. Use variables only for storing values that change.

a student will most likely follow.

But as I've personally seen, for a student this is just an order, a ritual. Do this because that's how it's supposed to be done. Actually understanding comes usually much later, when they for the first time screw up because some value got unexpectedly mutated. Granted, this is a minor thing and nothing like the scary concurrency monster that shows its head when you put a global in your otherwise innocent little console program. However, I think it illustrates that sometimes during the learning experience, you have to postpone things and just press on with other stuff for now.

5 Likes

GCD is easier to teach, maybe, but is much harder to use correctly. The reality of unstructured concurrency is that once newcomers get their feet in the water of GCD or similar API's, likely fail to write any useful and reliable code then turn away from it to come back to it only much later.

But I'll go a bit further and suggest a contrarian opinion. Computers have become more complex and so have programming languages. The era of linear progress is over, I think we are witnessing a non-linear qualitative change in computing, similar to the leap in physics in the early 20th century.

Nobody is complaining that modern physics is too hard to teach (let alone impossible to know in its entirety), because it's the way it is, you need to get a good understanding of physics and its mathematical apparatus if you want to create anything useful with the knowledge.

I believe the problem of progressive disclosure will be softened for Swift over time, it's a matter of experimentation and polishing of the compiler. Swift 6 is very new, it just needs some time. But it will probably never be your "Swift for newbies in two weeks" kind of a language :slight_smile:

5 Likes

I'm not sure I would say it's much harder to use correctly. You don't have the compiler checking for certain types of errors with GCD, but even with Swift Concurrency you only have the compiler checking for some types of errors. In both cases you're needing to learn one of the most complex parts of computing to a good enough level to not make mistakes. The difference with GCD is you can put off learning about the existence of that for longer (which is beneficial to teaching someone to code).

Computers have become more complex, but the problems the vast majority of people want to solve with them have not. Again, the vast majority of apps people will write with Swift barely need any concurrency. I have an decently sized app (30+ KLOC document-based Mac app) and I have maybe 2 pieces of concurrent code:

  • Cropping images in the background (a simple OperationQueue)
  • OCR using the Vision framework (a simple DispatchQueue.global(…).async)

Now I do use async/await a lot for various APIs, but while those are grouped into Swift Concurrency they exist primarily for asynchronous code, a concept which is FAR easier to reason about as a dev, far easier to teach to a new programmer, and which actually does have fairly reasonable progressive disclosure.

In many ways it feels like Swift is optimised more to be "theoretically correct" for certain edge cases, rather than ergonomically optimised for developer productivity (Concurrency is the prime example, but some recent posts on Mastodon relating to substring handling show it's not the only one). Yes, Swift's approach does lead to some more bugs being caught by the compiler or prevented by the APIs, but it does so by making the language and APIs more obtuse and requiring you to learn about the technical intricacies to understand why things work the way they do, when all a student wants and needs is for something to work at all. There are cons to the "just trust me" approach, but we need to remember that there are pros to the apparoch too.

To use your analogy of modern physics, then yes you need to learn a lot of stuff and be pretty good at Maths to get a good understanding of something like Relativity or Quantum Mechanics. But the thing is, we teach a LOT of physics without ever touching on those things, even sprinkling in a few "lies" to help kids understand (e.g. the planetary model of the atom). And when it gets to doing something professionally, the overwhelming majority of people barely need to know anything more than Newtonian physics, even if you're sending someone to the moon.

Just becaues a system is complex, doesn't mean we need to expose that complexity across every aspect of our interaction with that system (which I think is ultimately the point the OP is trying to make) :slight_smile:

8 Likes

Actually, the comparison between goto and structured branching is valid here. GCD is the goto of concurrency, too easy to make a mistake, whereas modern structured concurrency makes it harder to, say, hang on a semaphore indefinitely due to some logical mistake, let alone data integrity guarantees that Swift can give.

I may be wrong, but I presume it's because you didn't think of parallelization of your tasks much. Concurrency is mostly about putting multiple CPU cores to work. Not always easy to reason in these terms, also a lot of the times improvements are marginal, but sometimes they are significant and noticeable by the user.

In fact SwiftUI softly pushes you to think of parallelization because it's a functional system and as such it kind of invites concurrency into your code. Have you considered these things?

I think this post needs some clarification. I’m interpreting it to imply that this person doesn’t understand their own application’s design or needs, and I think that’s wholly inappropriate.

2 Likes

It's not that I don't think of concurrency, it's just that it's an optimisation. I usually only think about optimisations of any sort when I have evidence they are required, given they lead to increased complexity of code.

Ultimately though there is very little in the app that needs to be concurrent. There's stuff that could be but I simply choose not to. For example, when a user searches in a document I could check each record in the document in parallel rather than doing it in sequence on the main thread, but the code is fast enough and the data set small enough to not hang the main thread so I've never needed to. And if it did cause a problem I'd just move the sequential search algorithm to a background thread rather than immediately parallelise the task as it keeps the complexity to a minimum while solving the core issue.

And that's true of the vast majority of apps when it comes to concurrency. They only really need 2 threads most of the time: a UI thread (i.e. the main thread) and a background thread for long-running stuff that shouldn't block the UI. No amount of additional CPU cores is going to change that as the vast majority of code that is written is inherently not parallel, and a lot of what can potentially be parallelised is very difficult to do so correctly, for comparatively little benefit.

Concurrency, even utilising Swift Concurrency, introduces the risk of new bugs, ones that are harder to debug. The additional complexity also means it takes longer to implement a feature, time that could be spent on adding other features. You have to be sure that users will actually feel the benefit to an extent they care about to make up for these additional issues.

Now, this isn't to say there aren't situations where you can't extensively parallelise some code, or that there aren't apps that heavily utilise such code (I use several such apps all the time). It's just that those are the edge cases rather than the norm and (bringing it back to the topic of the thread) are cases that few need to learn about until they are well into their journey of learning to code.

As for SwiftUI, I don't use it and so far don't see much of a reason to start using it, but that's an entirely different topic :wink:

11 Likes

Interesting, the whole point of structured concurrency is to provide certain guarantees of correctness and thus minimize bugs. And that is my impression (and excitement) about it too.

SwiftUI is a functional system and as such requires a different kind of thinking and like I said it invites parallelism almost automatically, or let's say more naturally. Ever since I switched from UIKit to SwiftUI, I don' look back. With SwiftUI you write much less code and you don't have any auxiliary files for the UI part (like XIB's with UIKit/AppKit). It's an amazing invention and definitely a huge leap in programming.

There are definitely cases where programmers encounter data-race safety errors when they're not writing concurrent code. However, that doesn't mean we can't improve those cases to incorporate more progressive disclosure into the data-race safety model. I think we can! The ultimate goal is for the data-race safety checks to help beginners at the point when they're starting to learn about concurrent programming -- having to debug a nondeterministic data race is one of the most frustrating and discouraging things for a beginner to go through, and the language can prevent that. But learners shouldn't have to encounter concurrency concepts before they're ready.

Instead of philosophical debate about whether data-race safety in Swift 6.0 in its current form is good for beginners, I think this discussion would be much more constructive if it focused on exactly where programmers encounter data-race safety errors when they are not writing concurrent code. These are the cases that I already know are pain points and am looking at approaches for improving:

  • Global and static variables
  • Conformances of main-actor-isolated types to nonisolated protocols
  • Overrides of nonisolated superclass methods in a main-actor-isolated subclass
  • Nonisolated async functions always switching off of an actor to run

I know there are other pain points with task ordering and actor re-entrancy once you explicitly start writing concurrent code using tasks and actors, but I think this is a separate exploration, and those are clearly not the issues that beginners learning to code would struggle with.

34 Likes

Concurrency adds many classes of potential bugs. Swift Concurrency does a lot of work to prevent data race bugs but doesn’t anywhere near as much to eliminate many other classes of bugs (e.g. deadlocks). It’s an amazing feat that Swift is able to provide fairly comprehensive coverage for even one class of concurrency bugs, but if you go into it assuming the Swift compiler will stop you from creating any concurrency bugs you’re setting yourself up for failure.

It’s just how the Swift compiler can prevent bugs of passing in the wrong type, but it can’t validate the value of the type is correct. There’s only so much a compiler can do.

SwiftUI gels well with how some people think but personally I prefer the *Kit APIs and resulting code. But the learnability of different UI APIs is beyond the topic of the learnability of Swift