Last active
January 15, 2023 21:52
-
-
Save ts95/0ccc8c6b6d2469a7f1b49532a214fcdd to your computer and use it in GitHub Desktop.
Hashable & Codable struct-based cache implementation in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import OrderedCollections | |
protocol HasCacheCost { | |
var cacheCost: Int { get } | |
} | |
extension HasCacheCost { | |
var cacheCost: Int { | |
type(of: self) == AnyObject.self ? 0 : MemoryLayout.size(ofValue: self) | |
} | |
} | |
struct Cache<Key: Hashable & Codable, Value: Hashable & Codable>: Hashable, Codable { | |
var evictionPolicy: EvictionPolicy | |
private var store = OrderedDictionary<Key, Element>() | |
var count: Int { | |
store.count | |
} | |
var totalCost: Int { | |
store.values.reduce(0, { $0 + $1.cacheCost }) | |
} | |
var keys: any Collection<Key> { | |
store.keys | |
} | |
var values: any Collection<Value> { | |
store.values.lazy.map(\.value) | |
} | |
init(evictionPolicy: EvictionPolicy = .never) { | |
self.evictionPolicy = evictionPolicy | |
} | |
subscript(_ key: Key) -> Value? { | |
get { | |
store[key]?.value | |
} | |
set { | |
prune() | |
if let newValue { | |
let cacheCost = (newValue as? HasCacheCost)?.cacheCost ?? 0 | |
store[key] = Element(value: newValue, cacheCost: cacheCost) | |
} else { | |
store[key] = nil | |
} | |
} | |
} | |
subscript(_ key: Key, default defaultValue: Value) -> Value? { | |
get { | |
self[key] ?? defaultValue | |
} | |
set { | |
self[key] = newValue | |
} | |
} | |
func contains(key: Key) -> Bool { | |
keys.contains(key) | |
} | |
private mutating func prune() { | |
switch evictionPolicy { | |
case .maxCount(let maxCount): | |
assert(maxCount > 0, "maxCount must be greater than zero") | |
let delta = store.count - maxCount | |
if delta > 0 { | |
store.removeFirst(delta) | |
} | |
case .maxTotalCost(let maxTotalCost): | |
assert(maxTotalCost > 0, "maxTotalCost must be greater than zero") | |
var totalCost = self.totalCost | |
var numberOfElementsToRemove = 0 | |
for element in store.values.reversed() { | |
if totalCost <= maxTotalCost { | |
break | |
} | |
totalCost -= element.cacheCost | |
numberOfElementsToRemove += 1 | |
} | |
if numberOfElementsToRemove > 0 { | |
store.removeFirst(numberOfElementsToRemove) | |
} | |
case .never: | |
break | |
} | |
} | |
enum EvictionPolicy: Hashable, Codable { | |
/// Prunes oldest elements to maintain a count less than `maxCount`. | |
case maxCount(Int) | |
/// Prunes oldest elements when the total cache cost of values conforming to `HasCacheCost` exceeds `maxTotalCost` | |
/// until `totalCost` is less than `maxTotalCost`. | |
case maxTotalCost(Int) | |
/// Never prunes any cached data. | |
case never | |
} | |
struct Element: Hashable, Codable, HasCacheCost { | |
let value: Value | |
let cacheCost: Int | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment