파일과 풀은 정리 지점이 렉시컬이었다. LRU는 아니다 — 축출은 삽입의 부작용으로
예측할 수 없는 때에 일어난다. with를 구현하기 전에 알아야 할 자리였다.
D10 (큰 것) — Option/Result 패턴이 타입을 통째로 잃고 있었다.
match r { Ok(v) => v, Err(e) => e } // Int를 String 자리에 두는데 통과
typecheck의 env.ctors에 내장 생성자가 없어서 None이 "None이라는 이름의 변수
바인딩"이 되고 Ok(v)의 v가 TUnknown이 됐다. 즉 이 언어의 핵심 오류 처리
수단의 패턴 매칭이 타입을 전혀 검사하지 않고 있었다. 소진성 검사는 자기
is_ctor에 내장을 갖고 있어 이 사실을 덮었다 — 빠진 경우는 잡으면서 타입은
안 봤다. 내장 열거형을 등록해 고쳤고 기존 코드는 하나도 안 깨졌다.
여태 안 보인 이유: Ok(v) => Int.show(v) 같은 정상 코드는 v가 TUnknown이어도
통과하므로 아무도 이상함을 못 느낀다. 틀린 코드를 써봐야 드러난다.
D11 — struct 필드에서 affine 값을 꺼낼 수 없다. 필드 접근은 빌림이고 struct
분해 패턴이 없다. 열거형은 패턴이 분해하므로 가능하다. 즉 "둘을 함께
돌려주기"가 열거형으로만 된다. 튜플이 없는 대가가 표현 가능성 문제로 나왔다.
D12 — 축출은 렉시컬이 아니다. 그래서 자원이 렉시컬 전용이면 자원 컨테이너를
언어로 만들 수 없다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한
런타임 capability인 이유가 만들 수 없어서였다. (a) 렉시컬 전용을 택하고,
사용자가 자기 자원 풀을 못 만든다는 대가를 기록한다.
자료구조 자체의 마찰: Map이 없어 O(n), 튜플이 없어 운반용 struct 세 개,
get이 캐시를 새로 돌려줘야 함, 뒤에서 자르는 std 함수 없음.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
83 lines
2.5 KiB
Plaintext
83 lines
2.5 KiB
Plaintext
// 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],
|
|
}
|