///|
/// Wraps `f` so that each argument is computed once and remembered, the way
/// `functools.lru_cache` does it: the returned function keeps the last
/// `capacity` results and recomputes anything that fell out.
///
/// ```mbt check
/// test {
///   let mut calls = 0
///   let square : (Int32) -> Int32 = @cache.memoize(
///     key => {
///       calls += 1
///       key * key
///     },
///     capacity=8,
///   )
///   @test.assert_eq(square(4), 16)
///   @test.assert_eq(square(4), 16)
///   @test.assert_eq(calls, 1)
/// }
/// ```
pub fn[K : Hash + Eq, V] memoize(
  f : (K) -> V,
  capacity~ : Int32,
) -> ((K) -> V) raise @spec.Refused {
  let cache : LruCache[K, V] = LruCache::new(capacity~)
  key => {
    match cache.get(key) {
      Some(value) => value
      None => {
        let value = f(key)
        let _ = cache.set(key, value)
        value
      }
    }
  }
}