inout generic struct parameter error


(Georgios Moschovitis) #1

While getting my feet wet with Swift, I am still surprised with type-system ‘quirks’ here and there.

I tried to implement a simple array of weak references:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    var array: [Weak<T>]
    var index = 0
    
    init(_ array: [Weak<T>]) {
        self.array = array
    }
    
    mutating public func next() -> T? {
        while index < array.count && array[index].value == nil {
            // Remove weak references invalidated by ARC.
            array.remove(at: index)
        }
        
        if index < array.count {
            let value = array[index].value
            index += 1
            return value
        } else {
            return nil
        }
    }
}

public struct WeakArray<T: AnyObject>: Sequence {
    var weakRefs: [Weak<T>]
    
    init() {
        weakRefs = []
    }
    
    public var count: Int {
        return weakRefs.count
    }

    public subscript(index: Int) -> T? {
        get {
            return weakRefs[index].value
        }
        
        set(value) {
            weakRefs[index] = Weak(value!)
        }
    }
    
    mutating public func append(_ value: T) {
        weakRefs.append(Weak(value))
    }
    
    @discardableResult
    mutating func remove(at index: Int) -> T? {
        return weakRefs.remove(at: index).value
    }
    
    public func makeIterator() -> WeakArrayIterator<T> {
        return WeakArrayIterator(weakRefs)
    }
}

This kinda works but because we pass a struct at:

        return WeakArrayIterator(weakRefs)

the ‘garbage collection’ at:

            // Remove weak references invalidated by ARC.
            array.remove(at: index)

doesn’t affect the original array.

I tried changing to this:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    ...
    init(_ array: inout [Weak<T>]) {
        self.array = array
    } return WeakArrayIterator(&weakRefs)

public struct WeakArray<T: AnyObject>: Sequence {
    return WeakArrayIterator(&weakRefs)

but I get this strange (to me) error:

Cannot convert value of type ‘[Weak<T>]’ to expected argument type ‘[Weak<_>]’

I even get the fix-it suggestion:

Fix-it Insert “as [Weak<_>]”

which does not even compile!

I would appreciate it if someone could explain me this cryptic error and maybe provide some pointers about how to properly/efficiently implement a WeakArray.

thanks,
-g.


(David Sweeris) #2

FWIW, whenever I've encountered that message, it's been a sign that I'm either stretching type system a bit past what it can handle, or I've got a subtle error elsewhere in my code...

The passed-in array isn't going to get modified though, because you never change it. At least semantically speaking, arrays in swift are pass-by-value, so when you assign it to your struct's local storage and then later modify the *struct's* variable, only the local copy gets affected. If you want to mutate the passed-in array, you'll have to do it in the function you pass it to.

- Dave Sweeris

···

On Jan 1, 2017, at 10:48, Georgios Moschovitis via swift-users <swift-users@swift.org> wrote:

While getting my feet wet with Swift, I am still surprised with type-system ‘quirks’ here and there.

I tried to implement a simple array of weak references:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    var array: [Weak<T>]
    var index = 0
    
    init(_ array: [Weak<T>]) {
        self.array = array
    }
    
    mutating public func next() -> T? {
        while index < array.count && array[index].value == nil {
            // Remove weak references invalidated by ARC.
            array.remove(at: index)
        }
        
        if index < array.count {
            let value = array[index].value
            index += 1
            return value
        } else {
            return nil
        }
    }
}

public struct WeakArray<T: AnyObject>: Sequence {
    var weakRefs: [Weak<T>]
    
    init() {
        weakRefs = []
    }
    
    public var count: Int {
        return weakRefs.count
    }

    public subscript(index: Int) -> T? {
        get {
            return weakRefs[index].value
        }
        
        set(value) {
            weakRefs[index] = Weak(value!)
        }
    }
    
    mutating public func append(_ value: T) {
        weakRefs.append(Weak(value))
    }
    
    @discardableResult
    mutating func remove(at index: Int) -> T? {
        return weakRefs.remove(at: index).value
    }
    
    public func makeIterator() -> WeakArrayIterator<T> {
        return WeakArrayIterator(weakRefs)
    }
}

This kinda works but because we pass a struct at:

        return WeakArrayIterator(weakRefs)

the ‘garbage collection’ at:

            // Remove weak references invalidated by ARC.
            array.remove(at: index)

doesn’t affect the original array.

I tried changing to this:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    ...
    init(_ array: inout [Weak<T>]) {
        self.array = array
    } return WeakArrayIterator(&weakRefs)

public struct WeakArray<T: AnyObject>: Sequence {
    return WeakArrayIterator(&weakRefs)

but I get this strange (to me) error:

Cannot convert value of type ‘[Weak<T>]’ to expected argument type ‘[Weak<_>]’

I even get the fix-it suggestion:

Fix-it Insert “as [Weak<_>]”

which does not even compile!

I would appreciate it if someone could explain me this cryptic error and maybe provide some pointers about how to properly/efficiently implement a WeakArray.


(Ben Cohen) #3

Hi Georgios,

Yes, that isn’t the best of error messages.

The problem is that makeIterator() is not a mutating function, so inside it all properties are treated as immutable. This means you aren’t allowed to pass them into functions as inout arguments. It’s essentially the same as this:

func takesInout(i: inout Int) { i += 1 }

struct S {
    var x: Int
    func nonmutating() {
        takesInout(i: &x)
    }
}
// Error: Cannot pass immutable value as inout argument: 'self' is immutable
// Fix-it: Mark method mutating to make 'self' mutable

Ideally you’d get an error message along these lines for your case but it looks like it’s failing.

So, you might consider making makeIterator mutating like the Fix-it suggests, which would allow you to pass its members as inout arguments:

    mutating public func makeIterator() -> WeakArrayIterator<T> {

But if you do this you will get a different compiler error, because your struct will no longer conform to Sequence, which requires makeIterator to be non-mutating. This is important, especially assuming your array is eventually going to conform to Collection as well. One of the requirements of collections is that they give an accurate count, but in the case of your array, the count returned might be different (greater) than the number of elements returned when you actually iterate the array, and this is not allowed (you may find that some standard library functions will trap when they discover that this has happened).

HTH,
Ben

···

On Jan 1, 2017, at 10:48 AM, Georgios Moschovitis via swift-users <swift-users@swift.org> wrote:

While getting my feet wet with Swift, I am still surprised with type-system ‘quirks’ here and there.

I tried to implement a simple array of weak references:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    var array: [Weak<T>]
    var index = 0
    
    init(_ array: [Weak<T>]) {
        self.array = array
    }
    
    mutating public func next() -> T? {
        while index < array.count && array[index].value == nil {
            // Remove weak references invalidated by ARC.
            array.remove(at: index)
        }
        
        if index < array.count {
            let value = array[index].value
            index += 1
            return value
        } else {
            return nil
        }
    }
}

public struct WeakArray<T: AnyObject>: Sequence {
    var weakRefs: [Weak<T>]
    
    init() {
        weakRefs = []
    }
    
    public var count: Int {
        return weakRefs.count
    }

    public subscript(index: Int) -> T? {
        get {
            return weakRefs[index].value
        }
        
        set(value) {
            weakRefs[index] = Weak(value!)
        }
    }
    
    mutating public func append(_ value: T) {
        weakRefs.append(Weak(value))
    }
    
    @discardableResult
    mutating func remove(at index: Int) -> T? {
        return weakRefs.remove(at: index).value
    }
    
    public func makeIterator() -> WeakArrayIterator<T> {
        return WeakArrayIterator(weakRefs)
    }
}

This kinda works but because we pass a struct at:

        return WeakArrayIterator(weakRefs)

the ‘garbage collection’ at:

            // Remove weak references invalidated by ARC.
            array.remove(at: index)

doesn’t affect the original array.

I tried changing to this:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    ...
    init(_ array: inout [Weak<T>]) {
        self.array = array
    } return WeakArrayIterator(&weakRefs)

public struct WeakArray<T: AnyObject>: Sequence {
    return WeakArrayIterator(&weakRefs)

but I get this strange (to me) error:

Cannot convert value of type ‘[Weak<T>]’ to expected argument type ‘[Weak<_>]’

I even get the fix-it suggestion:

Fix-it Insert “as [Weak<_>]”

which does not even compile!

I would appreciate it if someone could explain me this cryptic error and maybe provide some pointers about how to properly/efficiently implement a WeakArray.

thanks,
-g.
_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Slava Pestov) #4

Nice catch. Here’s a minimal test case:

struct G<T> {}

func f1(_: inout G<Int>) {}
func f2<T>(_: inout G<T>) {}

func g1(t: G<Int>) {
  let _ = f1(&t) // cannot pass immutable value as inout argument: 't' is a 'let' constant
}

func g2(t: G<Int>) {
  let _ = f2(&t) // cannot convert value of type 'G<Int>' to expected argument type 'G<_>'
}

func g3<T>(t: G<T>) {
  let _ = f2(&t) // cannot convert value of type 'G<T>' to expected argument type 'G<_>'
}

In the second two cases, f2() is called, which has a generic type. Since the constraint system could not be solved, CSDiag won’t have a type substitution for T so the unresolved type prints as _ in the diagnostic. It looks like we bail out before checking if the failure is due to an ‘inout’ mismatch, and produce the bad generic diagnostic instead of the more specific one.

I filed https://bugs.swift.org/browse/SR-3525 if anyone wants to take a look.

Slava

···

On Jan 1, 2017, at 4:52 PM, Ben Cohen via swift-users <swift-users@swift.org> wrote:

Hi Georgios,

Yes, that isn’t the best of error messages.

The problem is that makeIterator() is not a mutating function, so inside it all properties are treated as immutable. This means you aren’t allowed to pass them into functions as inout arguments. It’s essentially the same as this:

func takesInout(i: inout Int) { i += 1 }

struct S {
    var x: Int
    func nonmutating() {
        takesInout(i: &x)
    }
}
// Error: Cannot pass immutable value as inout argument: 'self' is immutable
// Fix-it: Mark method mutating to make 'self' mutable

Ideally you’d get an error message along these lines for your case but it looks like it’s failing.

So, you might consider making makeIterator mutating like the Fix-it suggests, which would allow you to pass its members as inout arguments:

    mutating public func makeIterator() -> WeakArrayIterator<T> {

But if you do this you will get a different compiler error, because your struct will no longer conform to Sequence, which requires makeIterator to be non-mutating. This is important, especially assuming your array is eventually going to conform to Collection as well. One of the requirements of collections is that they give an accurate count, but in the case of your array, the count returned might be different (greater) than the number of elements returned when you actually iterate the array, and this is not allowed (you may find that some standard library functions will trap when they discover that this has happened).

HTH,
Ben

On Jan 1, 2017, at 10:48 AM, Georgios Moschovitis via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

While getting my feet wet with Swift, I am still surprised with type-system ‘quirks’ here and there.

I tried to implement a simple array of weak references:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    var array: [Weak<T>]
    var index = 0
    
    init(_ array: [Weak<T>]) {
        self.array = array
    }
    
    mutating public func next() -> T? {
        while index < array.count && array[index].value == nil {
            // Remove weak references invalidated by ARC.
            array.remove(at: index)
        }
        
        if index < array.count {
            let value = array[index].value
            index += 1
            return value
        } else {
            return nil
        }
    }
}

public struct WeakArray<T: AnyObject>: Sequence {
    var weakRefs: [Weak<T>]
    
    init() {
        weakRefs = []
    }
    
    public var count: Int {
        return weakRefs.count
    }

    public subscript(index: Int) -> T? {
        get {
            return weakRefs[index].value
        }
        
        set(value) {
            weakRefs[index] = Weak(value!)
        }
    }
    
    mutating public func append(_ value: T) {
        weakRefs.append(Weak(value))
    }
    
    @discardableResult
    mutating func remove(at index: Int) -> T? {
        return weakRefs.remove(at: index).value
    }
    
    public func makeIterator() -> WeakArrayIterator<T> {
        return WeakArrayIterator(weakRefs)
    }
}

This kinda works but because we pass a struct at:

        return WeakArrayIterator(weakRefs)

the ‘garbage collection’ at:

            // Remove weak references invalidated by ARC.
            array.remove(at: index)

doesn’t affect the original array.

I tried changing to this:

public struct WeakArrayIterator<T: AnyObject>: IteratorProtocol {
    ...
    init(_ array: inout [Weak<T>]) {
        self.array = array
    } return WeakArrayIterator(&weakRefs)

public struct WeakArray<T: AnyObject>: Sequence {
    return WeakArrayIterator(&weakRefs)

but I get this strange (to me) error:

Cannot convert value of type ‘[Weak<T>]’ to expected argument type ‘[Weak<_>]’

I even get the fix-it suggestion:

Fix-it Insert “as [Weak<_>]”

which does not even compile!

I would appreciate it if someone could explain me this cryptic error and maybe provide some pointers about how to properly/efficiently implement a WeakArray.

thanks,
-g.
_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Georgios Moschovitis) #5

Hey Ben,

Thank you for taking the time to provide this detailed explanation.
Very helpful indeed.

George.


(Georgios Moschovitis) #6

But if you do this you will get a different compiler error, because your struct will no longer conform to Sequence, which requires makeIterator to be non-mutating. This is important, especially assuming your array is eventually going to conform to Collection as well. One of the requirements of collections is that they give an accurate count, but in the case of your array, the count returned might be different (greater) than the number of elements returned when you actually iterate the array, and this is not allowed (you may find that some standard library functions will trap when they discover that this has happened).

Btw, isn’t it strange that the next() method in `IteratorProtocol` *is* mutating, and makeIterator() is not?


(Ben Cohen) #7

Iterators are inherently stateful things. But for collections, the state they are mutating is their own progress through the collection as you iterate – they aren’t mutating the collection itself. That is what their mutating keyword on next() is indicating. On the other hand, creating a fresh one from the collection by calling makeIterator() shouldn’t need to mutate the collection.

···

On Jan 2, 2017, at 1:52 AM, Georgios Moschovitis <george.moschovitis@icloud.com> wrote:

Btw, isn’t it strange that the next() method in `IteratorProtocol` *is* mutating, and makeIterator() is not?