13

I'm not a Swift programmer, so I have no clue about the language. I'm just asking out of curiosity. I came across a language construct where self inside a mutating function gets assigned a new value like in the function moveByAssigmentToSelf in the example below.

struct Point {
    var x = 0.0
    var y = 0.0

    mutating func moveByAssigmentToSelf(_ deltaX: Double, _ deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }

    mutating func moveByMutatingMembers(_ deltaX: Double, _ deltaY: Double) {
        self.x += deltaX
        self.y += deltaY
    }
}

Coming from a C/C++/Java/C# background I think of self as a pointer (address) to the (start of) the struct value somewhere in memory (e.g. on the stack). To me, assigning to something that should be equivalent to this in a C++ struct looks very strange. AFAICT, the function moveByMutatingMembers should have an observationally equivalent effect and would be the natural thing to do in the C++/Java world.

Can someone explain to a non-Swift programmer what is the rationale / idea behind this concept?

All I can find in the language reference (the chapter on expressions) are the following vague statements: "In an initializer, subscript, or instance method, self refers to the current instance of the type in which it occurs." and "In a mutating method of a value type, you can assign a new instance of that value type to self."

What I'm trying to understand is why is that assignment a good idea, which programming problems does it solve compared to the conventional solution?

In other words: Why does this make sense from a language-design perspective? Or: What would you lose if you had to do it the C++/Java way?

BTW, out of curiosity, I had a look at the Godbolt disassembly of this example and for my untrained eyes the output for moveByAssigmentToSelf looks horribly inefficient compared to the alternative.

Stefan Zobel
  • 3,182
  • 7
  • 28
  • 38
  • 3
    "Coming from a C/C++/Java/C# background I think of self as a pointer " - two and a half of those languages do not make self a pointer – user253751 May 03 '23 at 08:38
  • 2
    While `this` is a pointer in C++, it just as easily [could have been a reference](https://stackoverflow.com/q/645994/3022952). – R.M. May 03 '23 at 15:40
  • 2
    C doesn't even have an implicit `this` or `self`; IDK why you even mention C. – Peter Cordes May 03 '23 at 22:36

3 Answers3

18

Coming from a C/C++/Java/C# background I think of self as a pointer

This is incorrect. Since this type is a value type (a struct), self is a copy of that value. The power of mutating is that it will, at the end of the function, replace the previous value with the new value. Values do not have "instances." They're just values.

For a useful introduction to value types in Swift, see Value and Reference Types. Also useful is Structures and Enumerations Are Value Types from the main Swift documentation.

Swift structs are somewhat like C++, if you let go of preconceptions about new and pointers and references, and just consider structs to be structs. They're passed as values (copies), just like C++ structs. It's just that in Swift this is the normal way to do things, unlike in C++, where things are generally passed by reference.

To your question about efficiency, that's just because you didn't turn on optimizations. With optimizations, they're literally the same code, and they inline the call to the init:

output.Point.moveByAssigmentToSelf(Swift.Double, Swift.Double) -> ():
        movupd  xmm2, xmmword ptr [r13]
        unpcklpd        xmm0, xmm1
        addpd   xmm0, xmm2
        movupd  xmmword ptr [r13], xmm0
        ret

output.Point.moveByMutatingMembers(Swift.Double, Swift.Double) -> ():
        jmp     (output.Point.moveByAssigmentToSelf(Swift.Double, Swift.Double) -> ())

Looking at your questions to @matt, I expect this may also help you understand the system a bit better:

var p: Point = Point(x: 1, y: 2) {
    didSet { print("new Point: \(p)")}
}

p.moveByMutatingMembers(1, 2)

This prints "new Point" just one time, because p is replaced (set) just one time.

On the other hand:

p.x = 0
p.y = 1

This will print "new point" twice because p is replaced twice. Calling a mutator on Point replaces the entire value with a new value.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 2
    Ah, that `-O` output makes me feel a lot better. I already guessed that I had missed a compiler switch. Upvoted. – Stefan Zobel May 02 '23 at 21:33
  • I get the value type part and I didn't even consider how structs are passed around. Let's just assume that it is a struct that sits on the stack. When I said `instance` I just meant the bytes the struct occupies. I find saying "in Swift this is the normal way to do things" a bit disappointing. – Stefan Zobel May 02 '23 at 21:34
  • 1
    My point is that those bytes on the stack are copied, just as in C++. By "the normal way" I mean that in C++ you generally would avoid that copy happen (by using a reference, smart-pointer, or even a raw pointer). In Swift, it is normal to let that copy that happen (and the data types are designed such that copying is not expensive). I think you're trying to jump ahead to how value types are implemented and optimized, and working back to the language's semantics (which is how C++ generally works). But Swift starts with the semantics (copied values) and works forward to compiler optimizations. – Rob Napier May 02 '23 at 22:06
  • 1
    Fair enough, I understand now what you meant. Nevertheless, `self` is not a `copy` of the value (as you said above) but a `representative` of that same value, right? (i.e., in effect its address?). – Stefan Zobel May 02 '23 at 22:11
  • 3
    No, it's a copy. The optimizer might work to avoid the copy as an implementation detail, but semantically it is absolutely a copy. Swift has many techniques (most famously copy-on-write in most stdlib data structures) to make those copies efficient. But no, it's a copy. To your questions about very large structs (that do not include collection types with CoW optimizations), yes, those can cause performance problems if you're not thoughtful about them. – Rob Napier May 02 '23 at 22:13
  • That's interesting. You are right, that I'm thinking in C++ terms. What I was looking for is exactly an explanation of those Swift semantics of values that makes sense to my C++ tortured mind. – Stefan Zobel May 02 '23 at 22:16
  • 1
    If you're very comfortable with the details of C++, then Swift actually should make a lot of sense. Imagine C++ if most things were structs, you almost never used pointer (including references or smart pointers), but `vector` and `map` were replaced with types that store their contents in a private reference-counted buffer that only gets copied when modified if there are more than one reference to it. That's basically Swift. – Rob Napier May 02 '23 at 22:19
  • 3
    Keep in mind that Swift has no canonical "language specification" of the kind that C or C++ has. Since the rise of the Swift Evolution process, you can often find more detailed explanations of specific features, but they are not in the form of amendments to a spec. Much of Swift is detailed in the form of WWDC videos or Swift forum discussions. Possibly still the most famous explanation of what Swift is trying to achieve is "The Crusty Talk": https://developer.apple.com/videos/play/wwdc2015/408/ – Rob Napier May 03 '23 at 19:00
5

Assigning to self is useful for several reasons, but your example does not illustrate any. While this would be better spelled by + and += operators, assigning to self would make sense in your example only if another method already existed which performed the work on a new value.

func moved(_ deltaX: Double, _ deltaY: Double) -> Self {
  .init(x: x + deltaX, y: y + deltaY)
}

mutating func move(_ deltaX: Double, _ deltaY: Double) {
  self = moved(deltaX, deltaY)
}

The other option for nonmutating and mutating pairs switches the work to the mutating variant:

func moved(_ deltaX: Double, _ deltaY: Double) -> Self {
  var point = self
  point.move(deltaX, deltaY)
  return point
}

mutating func move(_ deltaX: Double, _ deltaY: Double) {
  x += deltaX
  y += deltaY
}

See union for one example of that in action in the standard library. Your performance concerns are not unfounded but the __consuming you'll find there will soon put them to rest.

  • 1
    Slowly, it starts to begin to make sense to me. Might be the explanation I was looking for. I have to study that and let that sink in a bit. Upvoted. – Stefan Zobel May 02 '23 at 22:29
4

Structs do not actually mutate. Merely saying myPoint.x = 1.0 actually replaces the struct. Thus it is no big deal if a struct substitutes a new copy of the struct as self, because that's exactly what you do every time you assign into a property of the struct.

That is why you cannot assign into a struct instance referenced by a let variable; this would require assigning a new struct into that variable, and you can't because let signifies a constant.

let myPoint = Point()
myPoint.x = 1 // illegal, and now you know why

Contrast a class, which is mutable in place — and that's why you can assign into a property of a class instance referenced by a let variable.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    "myPoint.x = 1.0 actually replaces the struct". Is that always true? What if the struct is really large? Copying the whole struct and replacing the old value when only a 8 byte member needs to be changed sounds horribly inefficient to me. Nevertheless, upvoted. – Stefan Zobel May 02 '23 at 21:53
  • 2
    Just assume the struct is so big that it doesn't fit into a single xmm register? Why would the compiler load and store several of them when it is not necessary? – Stefan Zobel May 02 '23 at 21:56
  • 2
    "Merely saying myPoint.x = 1.0 actually replaces the struct." That statement doesn't seem to be true from an assembler perspective. Playing around with a larger struct in my example on Godbolt I see that only a part of the struct gets replaced. – Stefan Zobel May 02 '23 at 23:31
  • 3
    That's an optimization. Logically the entire struct is replaced. Looking at the assembly output doesn't tell you much about how Swift works; it just tells you what the optimizer was allowed to do that was equivalent *in this program* to how Swift actually works. If you add a `didSet` on the variable or property that holds the struct, you'll see that it is called every time you modify any part of the struct because the entire struct is one value. – Rob Napier May 02 '23 at 23:49
  • @Rob Napier I'm not at all surprised that `didSet` fires more than once but I think it doesn't prove that the _entire_ struct gets replaced. Anyhow, I think it's not related to my original question "why does `self = ...`" make sense ever when you could just mutate the individual struct members? On a tangent, the nice compiler optimization you showed also seems to break down when the struct gets larger. Have to get some sleep now ... – Stefan Zobel May 03 '23 at 01:05
  • I think you're thinking of the struct being replaced as something that must exist in the assembly output. It does not; it is often optimized away and the struct members are just mutated because the compiler can prove it's equivalent. But semantically the entire struct is replaced because it's a single value, just like an Int, which is also a struct. Yes, structs with many properties can be expensive to pass in Swift compared to other languages. There are techniques, such as internal storage classes, that can be used to improve that, but it can be a weakness in the language's performance. – Rob Napier May 03 '23 at 12:36
  • @RobNapier Quote: "semantically the entire struct is replaced because it's a single value". Is there some place in the official documentation (language reference) that states these ideas and related views as expressed by matt explicitly? – Stefan Zobel May 03 '23 at 16:38
  • 1
    If Swift is anything like C#, a better abstraction model would be to say that a storage location of structure type is a bunch of storage locations of member types, stuck together with duct tape. A struct assignment is syntactic sugar for bulk copying all of the individual fields, without regard for which ones are public or private. – supercat May 03 '23 at 17:19
  • 3
    See links in my answer. In particular "Structures and Enumerations Are Value Types." You may also find various docs in the compiler source helpful to understanding more technical parts of the implementation: https://github.com/apple/swift/blob/main/docs/Arrays.md https://github.com/apple/swift/blob/main/docs/proposals/ValueSemantics.rst https://github.com/apple/swift/blob/main/docs/OwnershipManifesto.md But matt's answer is 100% correct. Mutating a struct is equivalent to making a copy with that change, then assigning that entire copy back to the original, even if the optimizer simplifies it. – Rob Napier May 03 '23 at 18:49