Another way of doing this is to use the Dictionary
subscript operation that allows specifying a default value. Unfortunately this still requires a helper function to force things through the _modify
accessor, but it's slightly simpler:
func modify<T>(_ value: inout T) { }
extension Dictionary {
mutating func setIfNotAlready(_ value: @autoclosure () -> Value, forKey key: Key) {
modify(&self[key, default: value()])
}
}
Ideally the standard library would supply a version of this out of the box. I have been toying with something like this variant:
extension Dictionary {
mutating func memoizedValue(
forKey key: Key,
generator: (Key) throws -> Value
) rethrows -> Value {
...
}
}
(The name would be subject to heavy bikeshedding, I'm sure. I find it more useful to have the value generator supplied by a regular function argument rather than an @autoclosure
. This variant also returns the actual cached value, as that tends to prevent yet another lookup immediately following this operation. In this version I also have the key as an explicit argument -- this lets callers avoid capturing the key in the supplied closure, but perhaps that's too subtle a point to insist on.)
Sample usage in a silly dynamic programming example:
var fibTable: [Int: Int] = [:]
func fib(_ n: Int) -> Int {
fibTable.memoizedValue(forKey: n) { n in
guard n > 1 else { return n }
return fib(n - 1) + fib(n - 2)
}
}
Unfortunately, this does not actually work, and this demonstrates an inherent limitation of this general approach: attempts at recursion tend to violate the Law of Exclusivity. Sadly fibTable
is being exclusively accessed by memoizedValue
while the closure is executing, so the recursive calls lead to concurrent modify accesses, which are illegal.
print(fib(10)) // Simultaneous accesses to 0x1079d0250, but modification requires exclusive access.
However, as long as we're okay with this limitation, I think such a Dictionary
method would be a worthwhile addition!
Note: The original two-phase solution doesn't have the same problem, as the recursion happens outside of accesses to the table:
var fibTable: [Int: Int] = [:]
func _fib(_ n: Int) -> Int {
guard n > 1 else { return n }
return fib(n - 1) + fib(n - 2)
}
func fib(_ n: Int) -> Int {
if let cached = fibTable[n] { return cached }
let result = _fib(n)
fibTable[n] = result
return result
}
print(fib(10)) // ⟹ 55
(Note how the table may receive an arbitrary number of insertions between the initial lookup of cached
and the final setting of result
-- this is another reason why we cannot generally do this with a single hash table lookup: the table may well be resized in between the two stages.)
(There are clever ways to try automating this memoization of such recursive functions via a construct not unlike a property wrapper; however I suspect that we can't really express that yet in Swift. Or can we?
)