Dictionary subscript extension without Index

Assuming I have a Protocol Identifiable, which enforces, that every implementing object has a readable Id-string property, would it at all be possible, to extend Dictionary to accept new elements without explicitly providing an Index, but store the element under it's own Id?

Pseudo Code:

class Object : Identifiable {}
var object = Object(id: "123")
var dictionary = Dictionarystring:Identifiable()
dictionary = object

...which saves me doing dictionary[object.id] = object

I think it could be easily done with a method, but using a subscript would feel more natural.

What is your last line of pseudocode meant to read as? To me it looks like
dictionary 'box glyph' = object

Is dictionary[object.id] = object that bad?

Such a subscript would be write-only, so I don't think it's "natural". And Swift doesn't allow write-only properties and subscripts. One of the reasons for this is that you should be able to do this

var p: (a: Int, b: Int) {
  get { ... }
  set { ... }
}

p.a = 42

And that requires both getter and setter because it's essentially equivalent to this

var temp = p
temp.a = 42
p = temp

So, while it's theoretically possible to have write-only properties and subscripts, the constraints that should be applied to them make them hardly usable.

2 Likes

Could be better. Should match objects.keyed(by: \.id).

final class Object: Identifiable { }
let object = Object()
var dictionary: [Object.ID: Object] = [:]
dictionary.set(object, for: \.id)
#expect(dictionary[object.id] === object)
extension Dictionary {
  mutating func set(_ value: Value, for getKey: (Value) -> Key) {
    self[getKey(value)] = value
  }
}

Honestly, I'd recommend just writing dictionary[object.id] = object. It's clear and obvious to any reader.

dictionary[] = object is not possible, even if you write a subscript whose key is Void (that would still require you to write dictionary[()].

I probably wouldn't even write a method for this, the indirection would just add confusion, and the method wouldn't really simplify anything.

If you’re writing your own type wrapping the dictionary, insert(_:) would be a reasonable name for this operation. But adding a method like that to Dictionary itself would be confusing to me if I were to read code that used it.

1 Like

I would say that it does, practically, as long as the copy you showed does not happen.

E.g.

dictionary.set(object, for: \.id)

above, is replaceable with

dictionary[] = object
extension Dictionary where Value: Identifiable, Key == Value.ID {
  subscript() -> Value {
    @available(*, unavailable) get { fatalError() }
    set { self[newValue.id] = newValue }
  }
}

(I don't think doing this is a good idea though.)


Why does this even compile?

var p: (a: Int, b: Int) {
  @available(*, unavailable) get { fatalError() }
  set { }
}
p.a = 42 // Crashes at runtime.

It doesn't compile if a tuple isn't involved.

struct P { var a, b: Int }
var p: P {
  @available(*, unavailable) get { fatalError() }
  set { }
}
p.a = 42 // Getter for 'p' is unavailable
2 Likes

I think maybe you're getting confused with parameter packs? It should work there too; it's just blocked by one of their many bugs.

enum E<each T> {
  static func f(_: repeat each T) -> Void { () }
  static subscript(_: repeat each T) -> Void { () }
}

E<Void>.f(())
E<Void>[()]
E< >.f()

// Everything above compiles.
// But this crashes the compiler.
E< >[]

The real use case for empty subscripts is probably just using all of the available default arguments.

struct S {
  subscript(x: Int = 0, y: Int = 0) -> some Any { () }
}

S()[]
3 Likes

Technically, you can do this if you default the parameter, but please don't.

extension Dictionary where Value: Identifiable, Key == Value.ID {
  public subscript(_: () = ()) -> Value {
    @available(*, unavailable) get { fatalError() }
    set {
      self[newValue.id] = newValue
    }
  }
}

struct Foo: Identifiable {
  var id: Int
}

func test(_ dict: inout [Int: Foo], _ newValue: consuming Foo) {
  dict[] = newValue
}

Edit: Subscripts aren't required to have an index parameter? I didn't know that.

1 Like

Empty subscript is fine, I use it as an alias for UnsafePointer.pointee in my personal projects. I agree that the lack of a getter makes me question whether it’s a good idea here, though. I also agree that marking the getter unavailable should not be ignored entirely—either it should be a de facto way to get set-only storage declarations, or the compiler should at least warn that the attribute will be ignored in that position.

Edited for brevity from source. pointee could have been avoided by an = 0. I bet it was decided against because people don't teach each other to recognize empty square brackets. :dotted_line_face:

extension UnsafePointer where Pointee: ~Copyable {
  @_alwaysEmitIntoClient public var pointee: Pointee {
    @_transparent unsafeAddress { self }
  }
}

extension UnsafePointer where Pointee: ~Copyable {
  @_alwaysEmitIntoClient public subscript(i: Int) -> Pointee {
    @_transparent unsafeAddress { self + i }
  }
}