Dictionary initialization refinement suggestion

Can we do a minor change to swift to treat optional values in dictionary initialisers as "no value"? Example below demonstrates the issue, where instead of a nice and concise first version I have to drop to a more verbose second version which also has an unfortunate consequence that the value becomes "var" (even if it could have been "let"). Besides it's more error prone (e.g. possible to make a typo and use the same key twice, which would be impossible in the first version of the code due to a runtime check for duplicated dictionary keys):

func foobar() {
    var foo: String
    var bar: String?
    var baz: String?
    
    let elements: [String: String] = [
        "foo" : foo,
        "bar" : bar, // Error: Cannot convert value of type 'String?' to expected dictionary value type 'String'
        "baz" : baz // Error: ditto
    ]
    
    var elements2: [String: String] = [
        "foo" : foo
    ]
    elements2["bar"] = bar
    elements2["baz"] = baz
    // Note that I can but I don't have to do the "if let" dance here, as dictionary
    // understands my intent here.
}

Not exactly what you're looking for, but this is what I use:

struct Attributes: ExpressibleByDictionaryLiteral {
	var dictionary: [String: String]

	init(dictionaryLiteral elements: (String, String?)...) {
		let keysAndValues = elements.compactMap { pair in
			pair.1.map { (pair.0, $0) }
		}
		dictionary = Dictionary(keysAndValues) { a, b in
			fatalError("non-unique key")
		}
	}
}

let attr: Attributes = [
	"abc": nil,
	"def": "ghi",
]
print(attr.dictionary)
2 Likes

Is Attributes useful outside of Dictionary initialization? I'd like to see more work put into literal expressibility, but without that, compactMapValues seems simpler to me.

let elements = Dictionary([
  "foo": foo,
  "bar": bar,
  "baz": baz
])
extension Dictionary {
  init(_ dictionary: [Key: Value?]) {
    self = dictionary.compactMapValues { $0 }
  }
}
2 Likes

To sum up we have these options:

var foo: String = ""
var bar: String?
var baz: String?

let elements: [String: String] = [
    "foo": foo,
    "bar": bar, // Error: Cannot convert value of type 'String?' to expected dictionary value type 'String'
    "baz": baz  // Error: Cannot convert value of type 'String?' to expected dictionary value type 'String'
]

// Workaround #1
let attributes1: Attributes = [
    "foo": foo,
    "bar": bar,
    "baz": bar
]
let elements1: [String: String] = attributes1.dictionary

// Workaround #1can also be turned into a more compact form with the help of `init(_ attributes: Attributes)`

// Workaround #2
let elements2 = Dictionary([
    "foo": foo,
    "bar": bar,
    "baz": baz
])

// Workaround #3
let elements3: [String: String] = [
    "foo": foo,
    "bar": bar,
    "baz": baz
].compactMapValues { $0 }

I'd still say that having this refinement of swift is worthwhile but obviously it's not pressing given we have these great workarounds.

Sometimes you might want a dictionary with optional values, so I wouldn't want to make this the default behavior, and then I think it's pretty subtle if it's based on a contextual type, whether it's explicitly written or not.

1 Like

It's true (probably in 1% of cases, but still), and the change is not going to amend this. Whether the type annotation is implicit or inferred - the old behaviour is preserved:

var foo: String
var bar: String?
var baz: String?

let elements: [String: String] = [
    "foo" : foo,
    "bar" : bar, // Error currently
    "baz" : baz  // Error currently
]

let elements: [String: String?] = [
    "foo" : foo,
    "bar" : bar, // currently OK
    "baz" : baz  // currently OK
]

let elements = [ // treated as [String: String?]
    "foo" : foo,
    "bar" : bar, // currently OK
    "baz" : baz  // currently OK
]

This is what I use. I feel like it's concise enough not to require more refinement. You can have a nil value, let and be explicit in one go.