By Brandon Kase / brandernan / @bkase_
Action | Milliseconds |
---|---|
Downloading over 3g | 500 |
Reading from disk | 50 |
Reading from memory | 1 |
protocol Cache {
associatedtype Key
associatedtype Value
func get(key: Key) -> Future<Value>
func set(key: Key, value: Value) -> Future<Void>
}
class RamCache<K, V>: Cache where K: Hashable {
typealias Key = K
typealias Value = V
func get(key: Key) -> Future<Value> { /* ... */ }
func set(key: Key, value: Value) -> Future<Void> { /* ... */ }
}
// md5 string key
class DiskCache<K>: Cache where K: StringConvertible {
typealias Key = K
typealias Value = NSData // byte array
func get(key: Key) -> Future<Value> { /* ... */ }
func set(key: Key, value: Value) -> Future<Void> { /* ... */ }
}
https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Boxkite.svg/2000px-Boxkite.svg.png
extension Cache {
func compose<B: Cache>(other: B) ->
BasicCache<Self.Key, Self.Value>
where Self.Key == B.Key
Self.Value == B.Value {
return BasicCache(
get: { k in
self.get(k).orElse{
other.get(k)
.map{ v in self.set(k, v); return v }
} }
}
set: { k, v in
Future.join(self.set(k, v), other.set(k, v)) }
}
}
let c = a.compose(b)
// c is a cache!
let c = a.compose(b).compose(x)
// c is a cache!
// always hit the network (no-op set)
class NetworkCache<K>: Cache where K: URLConvertible {
typealias Key = K
typealias Value = NSData // byte array
}
let c1 = (ram.compose(disk))
.compose(network)
// vs
let c2 = ram.compose(
(disk.compose(network))
)
Associative binary operator + an identity element = Monoid
let imageCache = fold(
ramCache,
diskCache,
networkCache
)
http://www.publicdomainpictures.net/pictures/170000/velka/wizard-1463307675N9Z.jpg
// bytesToImage: NSData -> UIImage
// imageToBytes: UIImage -> NSData
let imageNetCache: Cache<Value=UIImage> =
netCache.mapValues(bytesToImage, imageToBytes)
Note that these transformed caches are virtual.
They provide different projections onto the same underlying cache.
extension Cache {
func mapValues(
f: Circle -> Triangle,
_ fInv: Triangle -> Circle
) -> BasicCache<Key, Triangle> {
return BasicCache(
get: { k in self.get(k).map(f) }
set: { k, v in self.set(k, fInv(v)) }
)
}
}
extension Cache {
func mapValues(
f: NSData -> UIImage,
_ fInv: UIImage -> NSData
) -> BasicCache<Key, UIImage> {
return BasicCache(
get: { k in self.get(k).map(f) }
set: { k, v in self.set(k, fInv(v)) }
)
}
}
extension Cache {
func mapValues<V2>(
f: Value -> V2,
_ fInv: V2 -> Value
) -> BasicCache<Key, V2> {
return BasicCache(
get: { k in self.get(k).map(f) }
set: { k, v in self.set(k, fInv(v)) }
)
}
}
extension Cache {
func mapKeys<K2>(
fInv: K2 -> Key,
) -> BasicCache<K2, Value> {
return BasicCache(
get: { k in self.get(fInv(k)) }
set: { k, v in self.set(fInv(k), v) }
)
}
}
// diskCache: DiskCache<String>
// urlToString: Url -> String
let urlCache = diskCache.mapKeys(urlToString)
let diskAndNet =
diskCache.compose(netCache)
let diskAndNetImage =
diskAndNet.mapValues(bytesToImg, imgToBytes)
return ramCache.compose(diskAndNetImage)
extension Cache where K: Hashable {
func reuseInflight(
dict: [K: Future<V>]) -> BasicCache<K, V> {
var dict = dict
return BasicCache(
get: { k in dict[k] ?? ({
let f = self.get(k)
dict[k] = f
return f
})() }
set: self.set
)
}
// logic for freeing the memory elided
}
let smartNetworkCache = networkCache.reuseInflight(dict)
// still conforms to Cache<Key=Url,Value=NSData>
let f = smartNetworkCache.get(url)
let f2 = smartNetworkCache.get(url)
// only one real network request!
// same reference f === f2
let optimizedCache = diskCache.compose(netCache)
.reuseInflight(dict)
let diskAndNet =
diskCache.compose(netCache).reuseInflight(dict)
let diskAndNetImage =
diskAndNet.mapValues(bytesToImg, imgToBytes)
return ramCache.compose(diskAndNetImage)
Purescript implementation created to help formalize these ideas
By Brandon Kase / brandernan / @bkase_
Slide Deck: https://is.gd/edLKW7
Thanks to Vittorio Monaco for making the Carlos library
Cache
protocol)associatedtype
.In Swift, type erasure is converting protocol constraints to a concrete struct full of lambdas. Existential associated types become universally quantified generics.
Type erasure is usually used in Swift to simplify type signatures, but we can also use it to workaround this limitation in the language. You erase the type information that you had about the specific cache and add a small runtime penalty of an extra function call
func simplify() -> BasicCache<K, V> {
return BasicCache(getFn: self.get, setFn: self.set)
}
struct BasicCache<K, V>: Cache {
typealias Key = K
typealias Value = V
let getFn: K -> Future<V>
let setFn: (K, V) -> Future<Void>
func get(key: Key) -> Future<Value> {
return getFn(key)
}
func set(key: Key, value: Value) -> Future<Void> {
return setFn(key, value)
}
}
// bytesToImageAsync: NSData -> Future<UIImage>
// imageToBytes: UIImage -> Future<NSData>
let imageNetCache: Cache<Value=UIImage> =
netCache.asyncMapValues(
bytesToImageAsync,
imageToBytesAsync
)
// asyncMapValues implementation similar to mapValues
v
appears in covariant output and contravariant input positionsk
appears in contravariant input positions