Skip to content

Instantly share code, notes, and snippets.

@ts95
Last active January 15, 2023 21:52
Show Gist options
  • Save ts95/0ccc8c6b6d2469a7f1b49532a214fcdd to your computer and use it in GitHub Desktop.
Save ts95/0ccc8c6b6d2469a7f1b49532a214fcdd to your computer and use it in GitHub Desktop.
Hashable & Codable struct-based cache implementation in Swift
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