// LRU 캐시 — 값 판. // // 먼저 자원 없이 써서 자료구조 자체의 마찰을 본다. 자원 판은 resources.cool. import "cool.dev/std/list" as List // ※ Option[Int]의 Option은 내장 타입이라 import가 필요 없다. pub copyable struct Entry { key: String, value: Int, } // 최근에 쓴 것이 앞. Map이 없어 리스트를 훑는다 — O(n)이다. // LRU의 요점이 O(1)인데 그것을 표현할 수단이 언어에 없다. pub copyable struct Cache { cap: Int, entries: List[Entry], } pub fn empty(cap: Int) -> Cache { Cache { cap: cap, entries: [] } } // get이 최근성을 바꾸므로 캐시를 새로 돌려줘야 한다. // 그런데 값도 같이 돌려줘야 한다 — 튜플이 없어 struct를 하나 더 만든다. pub copyable struct Got { cache: Cache, value: Option[Int], } pub fn get(c: Cache, key: String) -> Got { let hit = List.first(List.filter(c.entries, fn(e) { e.key == key })) match hit { None => Got { cache: c, value: None }, Some(e) => Got { cache: Cache { cap: c.cap, entries: touch(c.entries, key, e) }, value: Some(e.value), }, } } // 찾은 항목을 앞으로 옮긴다. pub fn touch(entries: List[Entry], key: String, e: Entry) -> List[Entry] { List.concat([e], List.filter(entries, fn(x) { !(x.key == key) })) } pub fn put(c: Cache, key: String, value: Int) -> Cache { let without = List.filter(c.entries, fn(x) { !(x.key == key) }) let added = List.concat([Entry { key: key, value: value }], without) Cache { cap: c.cap, entries: evict(added, c.cap) } } // 넘치면 뒤에서 떨어뜨린다. 값이라 그냥 사라진다 — 자원이면 이야기가 다르다. pub fn evict(entries: List[Entry], cap: Int) -> List[Entry] { if List.len(entries) <= cap { entries } else { evict(drop_last(entries), cap) } } // 마지막 하나를 뺀 리스트. 뒤에서 자르는 함수가 std에 없다. pub fn drop_last(entries: List[Entry]) -> List[Entry] { List.reverse(drop_first(List.reverse(entries))) } pub fn drop_first(entries: List[Entry]) -> List[Entry] { List.fold(entries, Dropping { first: true, kept: [] }, fn(own d, e) { if d.first { Dropping { first: false, kept: d.kept } } else { Dropping { first: false, kept: List.push(d.kept, e) } } }).kept } // fold에 "첫 원소 건너뛰기"가 없어 운반용 struct를 또 만든다. pub copyable struct Dropping { first: Bool, kept: List[Entry], }