How to define a custom signature array operator that makes array elements "spring available" when needed
It has nothing to do with the operator index, but more of a question about how to define the operator ??=
. What you can do, but it may not work as you expect.
There is a possible implementation:
// first define the ??= operator
infix operator ??= { }
// then some pretty standard logic for an assignment
// version of ??
func ??=<T>(inout lhs: T?, rhs: T) {
lhs = lhs ?? rhs
}
This compiles and works as you would expect:
var i: Int? = 1
i ??= 2 // i unchanged
var j: Int? = nil
j ??= 2 // j is now Some(2)
It will also work in combination with indices:
var a: [Int?] = [1, nil]
a[1] ??= 2
a[1] // is now Some(2)
I say this may not work as expected due to the types. a ?? b
accepts an optional a
, and if it is nil
, returns a default value b
. However, it returns an optional value. That is the point ??
.
On the other hand, ??=
it cannot do it. Because the left side has already been defined as optional and the assignment operator cannot change the type, only the value. So when it replaces a value inside an optional in a case nil
, it won't change a type that is not optional.
PS The reason the function is ??=
compiled at all is that the optional values ββ(i.e. what you will return from lhs ?? rhs
) are implicitly updated to optional values, hence lhs ?? rhs
type T
> can be assigned lhs
which is of type T?
.
source to share
Unless you have a requirement for this "index extension" operation to be on Array
, you may find it easier to create your own type that wraps Array
(or another buffer) than to wedge this behavior into an extension Array
.
First, you were probably just thinking about using it array[array.count] = item
as a synonym for adding, right? (Other than that, it gets more complicated as @AirspeedVelocity notes, because it begs the question of what to add between the existing array elements and the new element.) Here's a version that does exactly that:
struct ExtendingArray<T>: ArrayLiteralConvertible {
typealias Element = T
var array: [T] = []
init(arrayLiteral elements: Element...) {
self.array = elements
}
subscript(index: Int) -> T {
get {
return array[index]
}
set(newValue) {
if index < array.count {
array[index] = newValue
} else if index == array.count {
array.append(newValue)
} else {
fatalError("can't assign subscript beyond append range")
}
}
}
}
// playground example
var foo: ExtendingArray = ["a", "b"]
foo[2] = "c"
foo // {["a", "b", "c"]}
If you want to create a rare collection where you can assign an item at any numeric index without having to fill in placeholders between non-matching indices ... you really don't want Array
- you want Dictionary
whose keys are integers. You can do it with everything Dictionary
if you like:
var sparse = [Int:String]()
sparse[0] = "a"
sparse[1] = "b"
sparse[26] = "z"
sparse // [0: "a", 1: "b", 26: "z"]
Or, if you want a little more array-like semantics, you can create your own type that wraps Dictionary
(and accepts ArrayLiteralConvertible
, a forward index into the underlying dictionary, etc.).
source to share
Since @MartinR points out that I've probably asked the question incorrectly, it answers another possible interpretation - you need an index that adds a value to the collection if not present.
You cannot do this with an operator. And you also won't be able to change the behavior of the standard index operator on an existing collection. But you can add a new index operator that takes a non-standard type. Therefore, you can try the following:
// custom type that is used to indicate behaviour
struct IfAbsent<I> {
let idx: I
init(_ idx: I) { self.idx = idx }
}
// add an array extension that defines a subscript that takes an IfAbsent type
extension Array {
subscript(idx: IfAbsent<Int>) -> T{
get {
return self[idx.idx]
}
set(newValue) {
if idx.idx < self.count {
self[idx] = newValue
}
else {
self.extend(Repeat(count: idx.idx - self.count + 1, repeatedValue: newValue))
}
}
}
}
var x = ["dkfkd", "dkff"]
x[IfAbsent(1)] = "does nothing"
x[IfAbsent(2)] = "will insert this"
// x will be equal to ["dkfkd", "dkff", "will insert this"]
The downside to arrays is to have values ββat every position, so you must put this new value in every record between the current last and target index:
var x = [1,2,3]
x[10] = 1
// x is now [1, 2, 3, 1, 1, 1]
Works much better for dictionaries:
extension Dictionary {
subscript(idx: IfAbsent<Key>) -> Value? {
get {
return self[idx.idx]
}
set(newValue) {
if self[idx.idx] == nil {
self[idx.idx] = newValue
}
}
}
}
You can also make the version that placed the array by default separate from the value to assign at a particular index:
struct WithDefault<T> {
let idx: Int
let dflt: T
init(_ idx: Int, _ dflt: T) {
self.idx = idx
self.dflt = dflt
}
}
extension Array {
subscript(idx: WithDefault<T>) -> T {
get {
return idx.idx < self.count
? self[idx.idx]
: idx.dflt
}
set(newValue) {
if idx.idx < self.count {
self[idx] = newValue
}
else {
self.extend(Repeat(count: idx.idx - self.count, repeatedValue: idx.dflt))
self.append(newValue)
}
}
}
}
It makes sense to have a reasonable default for the index get
:
var x = [1,2,3]
x[WithDefault(5, 99)] // returns 99
x[WithDefault(5, 0)] = 5
// x is now [1, 2, 3, 0, 0, 5]
(You can even get fancy and make a version that did the default closure that displayed the index).
source to share
If I can join the party :) Love responses from rickster and airspeed rate of ... extending the types of sequences and types of defaults ...
As posed, the question essentially describes the behavior of an ordered set (only provided by an operator, not a type). Therefore, we can play with this idea very simply:
infix operator ??= { }
func ??= <T: Hashable> (inout lhs: [T], rhs: T) {
if Set(lhs).contains(rhs) {
lhs += [rhs]
}
}
We could, however, strive to maintain the syntactic direction that Apple is taking with Swift and is based on its array concatenation operator +=
:
infix operator +== { }
func +== <T: Hashable> (inout lhs: [T], rhs: [T]) {
lhs += Set(rhs).subtract(lhs)
}
However, this does not preserve the relative order of elements in rhs
that are not in lhs
. To do this, we could:
func +== <T: Hashable> (inout lhs: [T], rhs: [T]) {
let dif = Set(rhs).subtract(lhs)
lhs += filter(rhs) { dif.contains($0) } // preserves order (fun version - not efficient!)
}
Which can be used like this:
var x = ["dkfkd", "dkff"]
x +== ["mmmm"] //--> ["dkfkd", "dkff", "mmmm"]
x +== ["and", "dkff", "so", "dkfkd", "on"] //--> ["dkfkd", "dkff", "mmmm", "and", "so", "on"]
source to share