Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does my struct become immutable in a method chain?

In Swift I am trying to implement a method "tap" similar to the method which exists in Ruby.

I've come up with the following example code:

private protocol Tap {
    mutating func tap(_ block: (inout Self) -> Void) -> Self
}

private extension Tap {
    mutating func tap(_ block: (inout Self) -> Void) -> Self {
        block(&self)
        return self
    }
}

extension Array: Tap {}

var a = Array(repeating: "Hello", count: 5)

a.tap {
    $0.append("5")
}.tap {
    $0.append("7")
}

print(a)  // (Expected) => ["Hello", "Hello", "Hello", "Hello", "Hello", "5", "7"]

I'm not super familiar with mutating functions, inout parameters, or Swift in general, but the code above looks like it should work to me. tap works as expected when it's not being included in a method chain. When I include it as part of a method chain, like in the above example, the Swift compiler complains:

Cannot use mutating member on immutable value: function call returns immutable value

Can anyone explain to me why this doesn't work? Can anyone provide a working solution and explain why that solution works?

Edit:

Another example usage would be:

let user = User(fromId: someId).tap {
   $0.firstName = someFirstName
   $0.lastName = someLastName
}

tap is a convenience thing that comes from Ruby. I'm mainly interested in understanding why the types in my function aren't working out right.

like image 230
G-P Avatar asked Nov 07 '22 21:11

G-P


1 Answers

The return self returns a copy of the original array, not the original array itself. Until this copy is stored as a var, it cannot be mutated. So, this would work:

var b = a.tap {
  $0.append("5")
}
b.tap {
  $0.append("7")
}

But not without storing b as a var first. Of course, you wouldn't create a b in the first place, you would just use a repeatedly as you already pointed out.

So, the issue is that you can accomplish tap once, but cannot chain taps. This is because the return of self is implicitly immutable, and you cannot call a mutating function on an immutable value. Changing tap to a non-mutating function could get you what you want:

private extension Tap {
    func tap(_ block: (inout Self) -> Void) -> Self {
        let copy = self
        block(&copy)
        return copy
    }
}

var a = Array(repeating: "Hello", count: 5)

a = a.tap({$0.append("5")}).tap({$0.append("7")})

Because each invocation of tap( returns a copy of the original modified by the given block, you can call it on immutable types. That means that you can chain.

The only downside is that new a = in the beginning.

like image 93
pmar Avatar answered Nov 15 '22 08:11

pmar