Dictionary subscript for value type default values

There is a note in dictionary subscript documentation that it doesn't work with class values.

At subscript(_:default:) | Apple Developer Documentation :

Do not use this subscript to modify dictionary values if the dictionary’s Value type is a class. In that case, the default value and key are not written back to the dictionary after an operation.

Unfortunately, the nature of this behavior is not the class itself but lack of modification thus write back to the dictionary with set:

Therefore this code doesn't work as one may expect:

var dict = [Int: String]()

let value = dict[1, default: "test"]

print(value) // prints `test`
print(dict) // prints `[:]` because no mutating operation occurred 

I wonder if that could be documented at the same page?

Other things that I was trying to use it as some analogue to c++ std::map::emplace(...) method that would add if element does not exist.

// avoid double lookup and hash calculation, create big object once:
_ = dict.addIfNotExists(key, /* autoclosure */ .init(...))

My main scenario is to create some big object just once.

So far I write the following way or wrap it to the function as above:

if dict[key] == nil {
    dict[key] = info
}

But I wonder if there is a better way doing this...

This should have had a mutating get to begin with. It's the only reason to ever bother with this subscript. If you don't want a mutating get, then just use ??. I'm surprised it got through review as-is, but it was a long time ago.

extension Dictionary {
  @inlinable subscript(
    key: Key,
    valueAddedIfNil value: @autoclosure() -> Value
  ) -> Value {
    mutating get {
      self[key]
      ?? { [value = value()] in
        self[key] = value
        return value
      } ()
    }
  }
  set { self[key] = newValue }
}
let test = "test"
var dict = [Int: String]()
#expect(dict[1, valueAddedIfNil: test] == test)
#expect(dict == [1: test])
dict[2, valueAddedIfNil: test].append(test)
#expect(dict[2] == "\(test)\(test)")
2 Likes

@Danny thanks.

Hm, I thought that this is smth in stdlib...
But found this topic instead which is probably more appropriate for my question: Assigning a dictionary without re-hashing - #18 by lorentey

Probably worth to follow up there..

Was it a typo and should have been if dict[key] == nil { ... }?
Otherwise as written above you could do:

dict[key]? = info
3 Likes

Right, this is a typo :grinning_face:

I think you are doing this right. Abusing subscript+default would look quite unnatural in this case (if worked).

There's a minute concern of calculating hash twice and doing lookup twice but unless your hash or EQ calculations are super-super heavy I'd not worry.

1 Like

Thank you for looking into it!

No, unfortunately (or fortunately?), it doesn't work. That was actually the thing that really confused me. I knew that adding classes (which present in official documentation). But it also didn't work but was never digging into details.

But turned, that adding value without modification never works for value types due to modify never called, i.e.:

let value = dict[1, default: "test"].count // or whatever non-mutating method is used

print(value) // prints `4`
print(dict) // prints `[:]`

I would say it is not intuitive and rather error prone. But now I think that maybe I should've put that in documentation section to add this note to the official docc here (same as for class): subscript(_:default:) | Apple Developer Documentation

Yeah, I would also not be much concerned about integers or other basic primitives.
And I see a solution from @lukasa to avoid hash re-calculation in other thread:

So, if I ever have the problem in hashing at this path I know what to do, so can put it to some helpers.
But I would vote to have this function in stdlib!

Seems pretty sensible to add a null-coalescing assignment operator ??= as suggested over in the other thread, which C# and JavaScript already have for example: dict[key] ??= info.

3 Likes