Extending arrays for specific element types

Hello!

I'm extending Array for a custom Element type to add a mutating function. However, I find myself unable to use the function, even on vars.
I get the following error:

Cannot use mutating member on immutable value: 'values' is immutable

Example code:

protocol Value {}

struct StringValue: Value {
    let value: String
}

struct IntValue: Value {
    let value: Int
}

extension Array where Element: Value {
    mutating func chain(_ newValue: Element) {
        if let last = self.last as? StringValue, let newValue = newValue as? StringValue {
            self = Array(self.dropLast())  // small edit
            self.append(StringValue(value: last.value + newValue.value))
        } else {
            self.append(newValue)
        }
    }
}

var values = [Value]()
values.chain(StringValue(value: "whatever"))  // compiler error on this line

I feel like I'm missing something, but can't tell what. All feedback is welcome :slight_smile:

2 Likes

You can't use Value as a concrete type for values. In this case you could use Any, an enum wrapper or an erased type that conforms to Value.

What is it you're trying to accomplish exactly?

Hi Dennis, I'm not sure what you mean. I have encountered no issue until this point setting the type of values as [Value].

For example, the following code runs without problems:

values.append(StringValue(value: "example"))
values.append(IntValue(value: 5))
for value in values {
    switch value {
    case let value as StringValue:
        print(value.value)
    case let value as IntValue:
        print(value.value)
    default:
        fatalError()
    }
}

About what I'm trying to do.
I have an heterogenous array that contains items of different types, all inheriting from the same protocol. Some of these types can be combined if they are next to each other (similar to my initial example).

To make the code cleaner, I wanted to define the function in an Array extension, but it doesn't work as expected and I would love to find out why.

The code works when defining the extension this way

extension Array where Element == Value

instead of like this

extension Array where Element: Value

I can't tell why this is the case. Ideas wanted!

2 Likes

I see. The issue is not the mutation (at least it's not here on my end); it's the usage of Value as a concrete type when you use it as a protocol. To tackle heterogeneous Arrays in a type safe way using enums:

public enum Value {
  case int (Int)
  case string (String)
}

extension Array where Element == Value {
  @inlinable public mutating func chain (_ newValue: Element) {
    switch newValue {
      case .string (let newString):
        if let oldValue = self.last, case .string(let oldString) = oldValue {
          self[self.endIndex - 1] = .string(oldString + newString)
        }

      case .int (let newInt):
        self.append(.int(newInt))
    }
  }
}

Using it:

var values: [Value] = [.int(0), .string("str")]

values.chain(.string("ing"))

print(values)
// [Value.int(0), Value.string("string")]

Another way using concrete types:

public protocol Value {}

extension String: Value {}
extension Int: Value {}

extension Array where Element == Value {
  @inlinable public mutating func chain (_ newValue: Element) {
    if let oldValue = self.last as? String, let newValue = newValue as? String {
      self[self.endIndex - 1] = oldValue + newValue
    } else {
      self.append(newValue)
    }
  }
}

Using it:

var values: [Value] = [0, "str"]

values.chain("ing")

print(values)
// [0, "string"]

The second solution is extendable. The first one isn't.

Thanks for your suggestions. It looks like we are not stumbling upon the same compiler messages :sweat_smile:

I am able to run the initial code I submitted after replacing the Element: Value by Element == Value without any issues (I'm running Swift 4.1.2).

Both the enum solution and extending the built-in types you propose don't fit my needs, however. The actual types I'm using are much more complex, and trying to fit them in enums or in extensions to built-in types would be unwieldy.

Thankfully I can run my intended code, although I still can't figure out why it works when defining Element in one way, but not in the other. :man_shrugging:

When you write,

extension Array where Element: Value

that applies to arrays where all the elements are the same type, and that type conforms to the Value protocol.

On the other hand, when you write,

extension Array where Element == Value

that applies to arrays whose elements are protocol existentials, meaning each element is a sort of “box” that can hold an instance of any type which conforms to the Value protocol.

Now, the important thing to note is that protocols do not conform to themselves. In other words, the “box” type does not conform to the Value protocol.

An instance of type Value *cannot* be used in a place that expects an instance of a type that conforms to the Value protocol, because the existential type Value does not conform to the protocol Value.

This might change in a future version of Swift, but for now that is how things work.

6 Likes

Thanks for your answer, this really clarifies the matter!

Pity that the compiler error was so misleading.

1 Like

It's fixed in Swift 4.2!

<stdin>:23:8: error: using 'Value' as a concrete type conforming to protocol 'Value' is not supported
values.chain(StringValue(value: "whatever"))  // compiler error on this line
       ^
5 Likes

Thank you, finally I found you! Works now for me.

What will be the best practice for that code. i.e should the extension of Array with specific element be at the same class file or as with all the extensiosn for the array