Introducing Swift in higher education

Hi everyone,

I’m proud to be able to finally announce something I’ve been working on for the past five years:

PWS Academy is an online education platform that aims to bring a high-quality computer science education to as many students as possible. We recognize that our industry has many problems and we want to do our part in solving them. By thinking outside of the box of traditional higher education, we aim to build high-quality courses and make them available to more students than ever before — especially those who don’t have access to affordable higher education in their area, and those who don’t have the privilege to study full-time.

This initial launch consists of just one course: “Programming with Swift: Fundamentals”. This is an introductory programming course, aimed at students with no prior programming experience. In my experience, this is the hardest audience to write for, and writing this course was one of my career goals.

Here’s what makes this course unique:

  • It’s actually aimed at beginners. New topics are introduced slowly, with great care, and with plenty of exercises. Advanced topics are postponed to future courses.
  • It adopts the principle of progressive disclosure and avoids forward references. Students should always feel confident about what they’ve learned so far, and should never get confused because a topic was only semi-explained or introduced too early.
  • The exercises and challenges are actually challenging. This course doesn’t just teach syntax; it focuses on building problem solving skills. Mindless copy-pasting will not be sufficient to complete this course.
  • Language features are introduced after the student has enough experience under their belt to truly understand them. For example: before a student learns about classes, they are tasked to build an entire game using only structures. By doing so, the student will encounter the need to share instances between multiple parts of the code, which will help them understand the need for classes.
  • The course emphasizes writing clear code, adopting best practices, and following a common coding style. Bad habits are hard to correct, so this course teaches good habits from the start.
  • The course goes beyond programming and includes an informal introduction to software analysis and design, so that students can learn how to tackle larger programming projects.
  • The entire course is fully platform-independent and can be completed on macOS, Linux, and Windows. In particular, the course doesn’t assume the student wants to be an iOS developer. It is part of a broader curriculum and lays the foundation for subsequent programming courses.
  • Last but not least: students have access to an instructor who can provide them with invaluable feedback, to correct their mistakes early on, and make sure they’re on the right track. While this does come at a cost, we try to keep everything as affordable as possible, to remain inclusive.

Get involved

Of course, this first course is only the beginning. My long-term goal for the academy is to be able to offer an accredited and well-respected Bachelor's degree, at a cost that won’t leave students in debt.

The next step towards this goal will be to recruit instructors and build out the curriculum. So if you’re reading this, you’re passionate about education, and you have expertise to share, please reach out! The Swift and iOS communities already have plenty of talented instructors, and I'm hoping to collaborate with you on the remaining courses that use Swift. Of course, the curriculum will also have plenty of courses unrelated to Swift or programming, so if you’re interested in teaching databases, operating systems, networking, AI, or anything else, feel free to contact me as well!

— Steven

PS: If you're not interested in becoming an instructor, but you want this project to succeed, feel free to give us a follow or retweet:

48 Likes

Congratulations on the launch! That looks so awesome!

The entire course is fully platform-independent and can be completed on macOS, Linux, and Windows.

Have you considered adding Android and Chromebooks to that list? It is easy to do with the Termux app for Android.

Good luck with this effort, it is an interesting idea. One suggestion: make one of the early chapters available for free, so people can sample before buying.

5 Likes

Have you considered adding Android and Chromebooks to that list?

I'd like to evaluate Chromebooks as well, but they're not very common here, so I haven't had a chance to use one.

Good luck with this effort, it is an interesting idea. One suggestion: make one of the early chapters available for free, so people can sample before buying.

Yes, that's on my wish list, but I don't have the infrastructure in place yet to do this :slight_smile:

I’d be very interested to know where you find this need crops up. As usual, I’m deeply skeptical of shared mutable state.

2 Likes

That's the thing you "unlearn" on the next level of progressive disclosure :wink:

FWIW, here are the stats of a real but small SwiftUI app:

number of classes - 5
number of structs - 89
number of enums - 14
number of lines ~ 4K

Interestingly, among these 5 classes three are the root classes and two are the subclasses of the other two of these five.

And these are quite different stats for another older swift UIKit based app:

number of classes - 260
number of structs - 260 (huh, I didn't do this on purpose!)
number of enums - 90
number of lines ~ 55K

Here about half of the classes are view/cell/controller subclasses.

1 Like

I love this. It's difficult to find 3rd party educational materials that are not in the context of iOS development.

2 Likes

We have to keep in mind the target audience here. These are students without any prior experience, so simply working with types is already a big step for them. W.r.t. types, my goal is only to give them a basic understanding of structs, enums, and classes. This happens near the end of the course, and the course ends shortly after, after a very brief introduction to protocols. In particular, this course does not cover:

  • declaring your own protocols
  • using protocols as existential types
  • class inheritance
  • generics
  • copy-on-write

In my experience, most student's brains are in need of a rest after learning about custom types. Teaching any of these advanced topics at this point will not have the desired result, so that's why I end the course there, and postpone any advanced topics to a later course, where students can start with a fresh mind.

W.r.t "Language features are introduced after the student has enough experience under their belt to truly understand them" and applying this to shared mutable state, here's how the course is structured to achieve that:

  • In Part II, the student learns about variables, control flow, and functions.
  • The student is then tasked to build a game of Tic-Tac-Toe. This game has a grid, which the student will most likely store as a series of variables position1, position2, ... This sets them up for learning about collections.
  • In Part III, students learn about collections and closures.
  • Then, the student is tasked to build a game of Blackjack. This is quite challenging, but it sets them up for learning about types. They will start to group related variables and functions, and will start to see "things" (instances of types) emerge from their code.
  • In Part IV, students first learn about structures. They are then asked to reimplement their game of Blackjack using types to declare the "things" they identified in their code.
  • Next, enumerations are introduced as a type-safe way to model "must be one of these"-things, such as suits of cards, which were most likely strings up until this point.
  • This will also be the student's first foray into API design. For example, to assign a card to a player, the student may use either card.assign(to: player) or player.assign(card). This kicks off the discussion of value types vs. references types, as students who try card.assign(to: player) will notice it doesn't work as intended, because player will be copied.
  • Finally, classes are introduced. The guideline I use here is: "If it represents something with an identity, and you want to share it, you probably need a class. Otherwise, stick to a value type." Students are then asked to audit all of their types, to see which ones should remain value types, and which ones can be classes. In many cases, both will work, and the student is asked to try both, just to see how it impacts the rest of their code.

I realize this is a very limited discussion of values vs. references, but it's as far as I want to go with this course. I can go a bit deeper in a follow-up course, but a truly in-depth discussion will probably have to wait until the student has a few years of experience under their belt. It may also require a different instructor, as I've spend all of my career in education, mostly focusing on the basics, so I don't have a lot of experience with designing large apps or libraries :slight_smile:

5 Likes

Personally I think value type programming is much more difficult than reference type programming, partly because the mainstream languages are OOP languages and most people get used to expressing relation by using reference.

That's probably the reason why shared mutable state is everywhere. For example, although value type is recommended in Swift, many people in this forum think that reference type should be used when one need to express identity (I'm not saying if this is right or not.).

A simple example. I wrote a financial planning app last year. The app has accounts and incomes, expenses and transfers. A transfer is associated with with two accounts. In the first version of the app, I implemented transfer and account using class. This was a natural decision and it was convenient. For example, if the user changes transfer date and amount, the two accounts associated with the transfer will get the change automatically. That's the benefit of shared mutable state.

I rewrote the app using value type this year (there is a technical reason why I'd like to do this, but that's another story). I found it required a very different mindset. It's strange I never see people mentioning this in the forum, but I found that using value type requires functional programming style. Otherwise it would be awkward to deal with complex relation among values.

But functional programming is hard, especially when mutating complex data structures. In my opinion reference type is like the goto statement. It's flexible and powerful, but it also make the code hard to reason about. Value type is like structured programming. It requires more thoughts but once you work it out, it's much more clear.

This is the hard-learned lessons from my own experience :sweat_smile: I know many people advocate value type in Swift, but I seldom see people discuss how to do it. I'm very curious what's other people's experience on this.

7 Likes

Congratulations on creating the course! I have taught Swift to Bachelor students for about 4 years and experienced the many challenges of the task.

Here is a tutorial I wrote in the early days. It is not up-to-date and only meant to be a lightweight support to my lectures, but you might perhaps find interesting bits: Swift Thoughts - Introduction to Swift programming


As for the whole value types vs reference types debate, I think your approach is interesting. One issue I have (although I have not read your course, only the summary you posted here) is that the difference is presented in a reference-oriented context.

As @rayx mentioned, however, programming with value semantics requires another mindset. One can wonder whether that mindset is better or worse than the one we use for reference semantics, but it's still different. Further, I would argue that its the mindset Swift actually promotes.

If your application relies on a graph where edges are references (which I suspect is how one is supposed to build the blackjack cards), then it is difficult to plug value types into the model without having to reshape everything, or at least do some handwaving to explain away why values are not necessarily a good fit in that particular instance. At that point, the argument for value semantics becomes much, much harder to make, because you have setup people to think in terms of references.

Even worse, you are not really showing why references are supposedly harder to reason about, you're just claiming it. But clearly, to the eye of the learner, the application is simple enough and they understand it perfectly. So why should they bother with value types when reference types can always do the job? Plus, if references are harder to reason about yet they can understand their code just fine, they simply must be exceptional programmers, right? That feeling of "yeah, that's bad practice but I know better" is really hard to eliminate in people who are no longer complete beginners.

In contrast, if we guide people toward a functional mindset, then value semantics is just natural and that is when introducing mutable value semantics becomes very interesting. Mutable value semantics lets you write updates of complex data structures very concisely (and in-place!), addressing one go @rayx criticisms. Meanwhile, pure FP compels you to write cumbersome (and inefficient) composition of updates, or get lost in the realm of monads.

If you introduce reference semantics in that functional context, then the problems show very quickly because you loose local reasoning! Therefore, it is much easier to show why Swift advocates for value types; the argument is no longer an abstract slogan.

I taught Swift as the implementation language for a class dedicated to formal modeling. My students were pretty much beginners. Although they were in their second year, they typically had very limited programming experience (1st year dedicated almost exclusively to math), and absolutely zero knowledge of statically typed languages. Nonetheless, I was able to introduce a significant part of Swift's type system (including generics and protocols) because it matched 1-to-1 the formal definitions they were studying at the same time. As a result, they were already building a functional mindset, and mutable value semantics was a natural feature to blend in.

My point is: I believe looking at programming from a more functional-oriented perspective has clear benefits toward understanding type systems and (mutable) value semantics, whereas going the imperative path tends to fallback on reference semantics due to our own hard-to-unlearn bias (it's likely that number of us have learned programming with "traditional" OO languages). This bias often shows in the examples we pick and the way we represent them, and it is extremely difficult to unlearn. Imperative approaches also tend to relegate types to a lesser role, because they place less emphasis on data and more on control, whereas functional programming is mostly about data and functions.

Disclaimer: research on education has yet to converge on a consensus w.r.t. to the best approach to teach programming, whether it should be imperative or functional, typed or untyped, etc. Until then, a teacher's own conviction will be the main driver. I am convinced that FP is a better fit, but I have nothing but my own experience and bias to justify that view.

7 Likes

Looks like a great resource for a true beginner.

Just one piece of feedback on the site itself; the screen shots are still quite small and difficult to see. It would be great to enlarge them if you click on them so get a more true to life preview.

Sorry, what's the thing you unlearn?

BTW, the stats tell us what people (typically?) do, but not much about what they need to do.

I'm afraid I don't see how reference types are involved here. This is merely about what's being mutated.

FWIW, that's not how I'd teach it. Value instances have identity; they're identified by the variables that hold them, and for subparts, the keypaths through which they are accessed. And “want to share it” is pretty fuzzy and not really objectively evaluable. So this guidance is IMO vacuous if you look at it too closely, and can easily lead a programmer into danger.

Here's a good example: I need to represent a social network. Okay, there are people; people have identity. And they have friends. Two different people can share the same friend. Obviously, I need classes and reference semantics, right?

Well, no, I can do something like this:

/// The identity of a person
typealias PersonID = Int

struct Person {
  var name: String;
  var address: String;
}

struct SocialNetwork {
  private var storage: [(Person, friends: [PersonID])] = []
  
  mutating func add(_ newMember: Person) -> PersonID {
    storage.append((newMember, []))
    return storage.count - 1
  }
  ...
}

The thing that a person object reference does that PersonID does not is to represent both the identity of a person (or a relationship, such as "is friend of"), and distribute shared mutable access to the person's data. It's the latter part that's seductive, but also problematic. IMO students would be best served by teaching them to distinguish these two roles, and to understand the costs of conflating them.

4 Likes

I'm 100% agreed that programming with value semantics requires a different style, and is hard for many people because they're not taught to do it. The culture of reference semantics is deeply embedded in the programming and academic communities.

But I wouldn't say it requires a functional style, which usually is taken to mean “data is immutable.” Yes, immutable types have value semantics, trivially, but the whole point of value semantics as it is expressed in Swift is that it keeps mutation accessible, while making it safe to use.

In my opinion reference type is like the goto statement. It's flexible and powerful, but it also make the code hard to reason about. Value type is like structured programming. It requires more thoughts but once you work it out, it's much more clear.

That's brilliant! Do you mind if I quote you?

9 Likes

Are there any recent books or resources that discuss how to start thinking in a "value semantic" way if you're experience is predominantly with reference semantics?

Example

When working in Swift, I've struggled to model some relatively common patterns in a value semantic way without seemingly making them needlessly complex. Consider a data structure that is representing hierarchical data:

struct Node { 
  var name: String
  var isExpanded: Bool
  var children: [Node]
} 

struct Tree { 
   var rootNodes: [Node]
}

It's quite natural to want to pass a Node around, particularly to a view. But any mutation of that Node is pointless because you're just mutating the local copy. Thus, anywhere in the code that needs to mutate a Node must have a way of accessing the component that owns the Tree and having it update the Node.

But how does the Tree know which Node needs updating? Inevitably you have to provide an id on every type, which in Swift sample code manifests itself as UUIDs all over the place.

struct Node { 
  // An identifier that doesn't really have any semantic meaning in the 
  // application other than to identify this instance. Any mutations to 
  // a Node must be careful not to accidentally regenerate this 
  // random identifier.
  var id: UUID
}

Given an id, how should the Tree find the Node to mutate? Does it traverse through all the nodes or does it maintain some sort of id to indexPath mapping? If the id is really all that matters, then should IDs be passed around the application instead of Nodes if mutation is a possibility?

extension Tree { 
  
  func updateNode(_ node: Node) { 
    // Should you just pass in the entire node and have 
    // the Tree find and replace it?
  } 

  func updateNode(id: Node.ID, node: Node) { 
    // Should you pass in an ID? Seems odd given 
    // that node has an ID so there's an invariant 
    // of id == node.id that should be true.
  }

  func updateNode(id: Node.id, isExpanded: Bool) { 
    // This doesn't really scale very well. 
  }
}

And when the Tree does mutate a Node, perhaps by toggling the isExpanded property, then there's chance of that triggering a potentially large copy-on-write operation.

Just one example where a reference type implementation is, in my eyes, relatively straigth forward, but a value type implementation is not. The benefits of value types are obvious to me, but their implementation has proven a challenge at times.

Would love to know how to improve designs like this and if there are any resources I can refer to for guidance.

FWIW, I have watched a lot of the C++ talks on YouTube that surface if you search for value types or value semantics.

(In hindsight, perhaps this comment should have been its own post. Happy to move it over if necessary.)

Don't know about that, but here's my take.

You don't have to use UUID if you don't want to, just autoincrementing Int will do.

The task at hand is traditionally done with references. Just consider that this is not a tree, but a graph... with this minor change you will have to use references somehow. However you can model these references via IDs and still stay within swift's value type world. Just beware it would be very unwise to have "var" fields in your Node, including things like name or expanded. var would be an invitation to change those fields directly in the Node while this is not what you want in the value type world.

Perhaps something like this will work for "value type" tree:

typealias NodeID = Int
typealias ParentID = NodeID

struct Node {
    private(set) var id: NodeID = Self.nextNodeID
    let parentID: ParentID?
    let name: String
    let isExpanded: Bool
    
    static var nextNodeID: Int {
        _nextNodeID += 1
        return _nextNodeID
    }
    private static var _nextNodeID = 0
}

struct Tree {
    private var nodes: [NodeID: Node] = [:]

    func node(for nodeID: NodeID) -> Node? {
        nodes[nodeID]
    }
    mutating func setNode(_ node: Node?, for nodeID: NodeID) {
        nodes[nodeID] = node
    }
    func parent(for node: Node) -> NodeID? {
        node.parentID
    }
    func children(of node: Node) -> [NodeID] {
        nodes.filter { $0.value.parentID == node.id }.map { $0.key }
    }
    mutating func setParent(_ parentID: ParentID?, for nodeID: NodeID) {
        guard let node = node(for: nodeID) else {
            print("node not found")
            return
        }
        let newNode = Node(id: node.id, parentID: parentID, name: node.name, isExpanded: node.isExpanded)
        setNode(newNode, for: nodeID)
    }
    mutating func setName(_ name: String, for nodeID: NodeID, parentID: ParentID?) {
        guard let node = node(for: nodeID) else {
            print("node not found")
            return
        }
        let newNode = Node(id: node.id, parentID: node.parentID, name: name, isExpanded: node.isExpanded)
        setNode(newNode, for: nodeID)
    }
    mutating func setExpanded(_ expanded: Bool, for nodeID: NodeID) {
        guard let node = node(for: nodeID) else {
            print("node not found")
            return
        }
        let newNode = Node(id: node.id, parentID: node.parentID, name: node.name, isExpanded: expanded)
        setNode(newNode, for: nodeID)
    }
    mutating func addChild(parentID: ParentID, childID: NodeID) {
        setParent(parentID, for: childID)
    }
    mutating func removeChild(parentID: ParentID, childID: NodeID) {
        setParent(nil, for: childID)
    }
    
    mutating func addNode(_ node: Node) {
        nodes[node.id] = node
    }
    mutating func removeNode(_ node: Node) {
        // TODO: handle childs (either remove or detach)
        nodes[node.id] = nil
    }
}

you may also consider moving "parentID" out of Node and have var parents: [NodeID: ParentID] in the Tree itself. I also didn't show how to get all roots of the tree (all nodes without parents) but that's trivial.

ps. in fact what i've creating above is not a tee but a graph, as i am not checking for cycles, so you can add your parent as a child. restricting this graph to a tree would be a simple change.

Heh, that's an interesting question; I don't know of any. I think Haskell's State monad basically puts you into a mutable value semantics programming model (@Alvae, please correct me if I'm wrong), so if you used that stuff a lot, you might get an appreciation for it. But I just did a quick search for examples and I'm afraid I can't really recommend the literature I'm finding. It's mostly obscured by jumping through the hoops implied by a language where you can somehow construe your mutation as actually non-mutating. I'm collaborating on a book with Sean Parent, and we're certainly going to address it, but that's in the future…

It's quite natural to want to pass a Node around, particularly to a view. But any mutation of that Node is pointless because you're just mutating the local copy.

I don't understand why you'd say that. If you're going to pass the node around for mutation, you pass it inout (or equivalently, as the self of a mutating method), and then the mutation isn't lost. The tree you defined at the top of the message is a perfectly good value-semantic tree (well, forest actually, since you have multiple roots). So this mutation will work:

extension Node {
  mutating func expandAll() {
    isExpanded = true
    for c in children { c.expandAll() }
  }
}

extension Tree {
  mutating func expandAll() {
    for n in rootNodes { n.expandAll() }
  }
}

Thus, anywhere in the code that needs to mutate a Node must have a way of accessing the component that owns the Tree and having it update the Node .

But how does the Tree know which Node needs updating? Inevitably you have to provide an id on every type, which in Swift sample code manifests itself as UUIDs all over the place.

Now that sounds like something that does actually happen… but misstated, at least if you're referring to what I think. What I'm thinking of is not about mutation at all. If you want to be able to traverse an arbitrary graph, the functions doing the traversal do need to have access to the whole graph, and there needs to be some representation of node identity. It doesn't have to be a UUID, though.

[Your tree doesn't have this problem because value whole-part relationships always have the shape of a tree, and you've used aggregation of parts to represent the parent-child relationship.]

The simplest representation of an arbitrary graph (just the relationships) is probably [[Int]], where each vertex is identified by an index in the outer array, and the inner arrays represent the destinations of outgoing edges on the corresponding vertex.

If you want to traverse this graph, you can never let go of the whole graph and just handle "a vertex." And that requires a shift in thinking if you're used to thinking of a network of objects. If a function needs to access a thing x, that thing either needs to be passed to the function (either by value or inout) or the function needs access to the thing y that x is a part of, and some directions to get to x in y. In this case, those directions are just an Int, but in general I guess they could be as complicated as an arbitrary KeyPath.

And when the Tree does mutate a Node , perhaps by toggling the isExpanded property, then there's chance of that triggering a potentially large copy-on-write operation.

There's no risk of that with the first Tree you defined unless you're storing copies of it.

Just one example where a reference type implementation is, in my eyes, relatively straigth forward, but a value type implementation is not. The benefits of value types are obvious to me, but their implementation has proven a challenge at times.

I'm afraid I don't understand why the value type implementation—of a tree, specifically—poses any problem at all, because of the whole-part tree shape mentioned above.

Would love to know how to improve designs like this and if there are any resources I can refer to for guidance.

FWIW, I have watched a lot of the C++ talks on YouTube that surface if you search for value types or value semantics.

Well, maybe watching Sean's talk on relationships would be helpful. It sounds like maybe what you're really struggling is how to represent relationships that are not a whole-part relationship.

(In hindsight, perhaps this comment should have been its own post. Happy to move it over if necessary.)

Sorry, my fault for bringing in the question of how to teach value semantics. If you want to start a new thread on that topic I'd be happy to go there.

3 Likes

I actually do use auto-incrementing integers in my apps, I just notice the use of UUIDs a lot more in sample code and blog posts.

It's certainly one way, though the API surface for Tree quickly becomes quite unwieldy with all the set functions. This isn't that practical if Node contains a lot of properties.

Ignoring the desire to have a parent reference, the Tree needs to do a lot of work to find, update and remove Nodes. Since a Nodes children are likely to be implement as a Swift Array, a very small change deep in the hierarchy can trigger quite the CoW back up to the root node.

The signature of this function is one that I've struggled with in my own implementations. Instinctively, I think nodeID should be passed in, but if node itself has an id property, then it doesn't really make sense to pass in the id twice. On the other hand, if the intent is to replace a node in the tree with a different node, then that would make sense but you never really replace anything if the id is different. (Rather you're doing a remove and an insert of two different nodes.)

I'm shocked to hear this. Why wouldn't you want to mutate a value in place? As I mentioned earlier, the whole point of value semantics as they are realized in Swift is that they make mutation easy and safe. Let me say this again:

:point_down: :point_down: :point_down:
:point_right: The whole point of value semantics in Swift is that it supports mutation :point_left:
:point_right: and local reasoning :point_left:
:point_up: :point_up: :point_up: :point_up: :point_up: :point_up: :point_up:

8 Likes

Right. IMO, in-place mutation and value semantics go hand-in-hand. The way I usually explain values is like this:

MyType is a value type, meaning that each variable is isolated from changes made to other variables.

Value semantics are all about enabling mutation, not prohibiting it. That's also why I don't buy the idea that you must use functional programming, or that it's a particularly good fit for value types. Personally, I don't very much enjoy FP and avoid it whenever I can, but I loooove value types.

1 Like