11

Is there a way to make the for .. in loop return references to the entries of a collection instead of copies?

Say I have an array points of CGPoint objects and I want to loop over them and pass each point to a function adjustPoint that can modify the point using an inout parameter.

Now doing the following doesn't work, since the for .. in loop returns the points as immutable / mutable (depending on whether or not I use var) copies of the actual points in the array:

for var point in points {
    adjustPoint(point: &point)  // This function only changes the copy
}

Currently, the only way I see to do this is to loop over the index:

for i in 0..<points.count {
    adjustPoint(point: &points[i])
}

Is this really the only way or is it also possible with a for .. in loop?

Note: I've read this question which is from quite some time ago (Swift 1 I believe) so I thought maybe they've changed something in the meantime: turn for in loops local variables into mutable variables

Community
  • 1
  • 1
Keiwan
  • 8,031
  • 5
  • 36
  • 49
  • Maybe you could try to wrap them in a class. – dasdom Sep 26 '16 at 16:29
  • 2
    Why not use map? You will be creating copies, but `points = points.map { adjustPoint(point: $0) }` will do the trick. – jjatie Sep 26 '16 at 16:34
  • Yes, I guess the functional approach is the next best alternative and keeps things clean but I still don't like the fact that Swift makes it so annoying to properly work with references and instead, you have all of these "one-time-use disposable" variable copies lying around everywhere. It's probably just personal preference though... – Keiwan Sep 26 '16 at 17:11
  • @jjatie Also, what if I don't want to update all but just the last n entries. Is there a way to do that using a map? – Keiwan Sep 26 '16 at 17:39
  • I think there's a misunderstanding of Swift Arrays here. You *never* really modify a Swift Array any more than you modify the number 4 when you add 1 to it. Every modification of an Array creates a new Array (like adding 1 to 4 creates a new Int). In some cases the compiler may be able to make that new copy very cheap (by proving that no one can see the old one, and so just mutating the old one in place), but logically, and in many cases actually, the Array after assignment is a completely different data structure than the one before. So there can't be references to individual elements. – Rob Napier Sep 26 '16 at 19:33
  • @Rob Napier Doesn't seem quite true -- `Array` implements `MutableCollection` so it is mutable. See my answer. – BaseZen Sep 26 '16 at 19:34
  • @BaseZen It is mutable, but as a *value* type. That means that if you took references to internal elements (which is difficult to do), when you modified them, they might not be the same Array (and doing that would be completely different than storing through a subscript.) – Rob Napier Sep 26 '16 at 19:35
  • This matters a lot in Swift. For instance, every time you mutate an Array property, its `didSet` method fires because the *entire* Array was replaced with a new one (it might technically be in the same memory due to compiler optimizations, but the system treats it as completely new). (Compare this to `NSArray`, which is a reference type. Modifying it does not behave the same.) – Rob Napier Sep 26 '16 at 19:36
  • Are you sure it's that extreme? I mean, even an `Int` is a value type, but when it is mutated, I would have every expectation that I'm using the same place in memory for the lifetime of that object. – BaseZen Sep 26 '16 at 19:38
  • There's no promise of that at all. It completely is dependent on whether there are other references to it. If there are, then it forces a "copy-on-write" the first time you modify it. This is one reason it's so hard to predict Swift performance behaviors. COW is very common in its value types. (See "Modifying Copies of Arrays" in the Array docs.) – Rob Napier Sep 26 '16 at 19:39
  • Also, that's not even true of integers. They can (and are) copied all the time and can refer to different locations (or optimized out of existence entirely, or be stored in registers such that they don't even have an address, and they can change which register). It just doesn't feel like they're "copied" because they're smaller than a machine word. But they still are. – Rob Napier Sep 26 '16 at 19:43
  • Your reference shows two *explicit* copies of an array, optimized with copy-on-write. You're saying *mutating a single copy in place* makes an *implicit* copy of the array. I don't see proof of that in the docs I have read thus far. In fact the docs seem to contradict what you claim, where it says: "Further modifications to `numbers` are made in place,..." – BaseZen Sep 26 '16 at 19:45
  • As an optimization they are. But logically it's a new value. Again, try adding a didSet to it. This even is important for inout parameters. They are not references. They are literally "in-out." At the beginning of the function they are copied in. Then various things may happen. And at the end they are copied out as a single event. (That's why didSet *doesn't* get called repeatedly if you modify a property that you pass in via inout. There's just a single assignment event.) This is discussed a lot in the "programming w/ value types" talk from last WWDC. Take a look at how Array is implemented. – Rob Napier Sep 26 '16 at 19:51
  • When I say "add a didSet to it," remember that you can attach didSet to local variables to explore when they are set. It doesn't have to be a property. – Rob Napier Sep 26 '16 at 19:53
  • No one ever claimed they are references. But when Apple docs explicitly state "modified in place", I read that as "modified in place". When you talk of registers such I think that just confuses the issue. We need to distinguish between a *language model* and the inevitable *compiler optimizations*. The language model of `a = b` is that `b` is a copy: if `b` is subsequently modified, `a` does not reflect these changes. The language model of `a[0] = 1` is that of mutation, or change in place: all future references to `a` will show the change. – BaseZen Sep 26 '16 at 19:54

1 Answers1

10

So the basic answer for your original for loop question is: no. The for...in is designed to give you copies of value types. It's a forced-functional programming style as you said yourself in the comments.

To mutate an array you must say array[index] in some fashion or another and now you're referring to the original value and can mutate it. The trick is finding an expressive way that prevents common errors. The four techniques I'm advocating below are:

  1. Make a powerful abstraction as an extension so you DRY throughout the code
  2. Use indices not a manual range which is also error prone (... vs. ..<)
  3. Avoid ugly throwbacks to C-language constructs like & (see #1)
  4. Consider keeping around the mutating version and the non-mutating version

This is probably most in keeping with the spirit of Swift, that is, quirky, verbose, and more nettlesome than you'd want, but ultimately very expressive and powerful with the proper layers in place:

import Foundation
import CoreGraphics

protocol Pointy {
    var x: CGFloat { get set }
    var y: CGFloat { get set }
    func adjustBy(amount: CGFloat) -> CGPoint
    mutating func adjustInPlace(amount: CGFloat) -> Void
}

extension CGPoint: Pointy {
    func adjustBy(amount: CGFloat) -> CGPoint {
        return CGPoint(x: self.x + amount, y: self.y + amount)
    }

    mutating func adjustInPlace(amount: CGFloat) -> Void {
        x += amount
        y += amount
    }
}

extension Array where Element: Pointy {
    func adjustBy(amount: CGFloat) -> Array<Pointy> {
        return self.map { $0.adjustBy(amount: amount) }
    }

    mutating func adjustInPlace(amount: CGFloat) {
        for index in self.indices {
            // mysterious chunk of type calculus: need  "as! Element" -- https://forums.developer.apple.com/thread/62164
            self[index].adjustInPlace(amount: amount) // or self[index] = (self[index].adjustBy(amount: amount)) as! Element 
       }
    }
}


// Hide the above in a Util.swift that noone ever sees.

// AND NOW the true power shows
var points = [ CGPoint(x: 3.0, y: 4.0) ]
points.adjustInPlace(amount: 7.5)
points.forEach { print($0) }
// outputs (10.5, 11.5)
let adjustedPoints = points.adjustBy(amount: 7.5) // Original unchanged
BaseZen
  • 8,650
  • 3
  • 35
  • 47