From 19b1dd9273e673ea02cc89d9582ee084608a8a0f Mon Sep 17 00:00:00 2001 From: "J.J" Date: Thu, 18 Apr 2024 22:51:53 -0400 Subject: [PATCH] v2.0.0: ETS implementation --- CHANGELOG.md | 10 ++++ README.md | 6 +- gleam.toml | 4 +- manifest.toml | 5 ++ src/rememo/ets/memo.gleam | 66 +++++++++++++++++++++ src/{rememo.gleam => rememo/otp/memo.gleam} | 5 +- 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/rememo/ets/memo.gleam rename src/{rememo.gleam => rememo/otp/memo.gleam} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d9143be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## v2.0.0 - 24-04-18 + +* Added the [Erlang Term Storage](https://www.erlang.org/doc/man/ets.html) implementation as `rememo/ets/memo`. This has reduced overhead compared to the original OTP implementation, which required message-passing to a `gleam_otp/actor` holding the memoization state. +* The original module was renamed from `rememo` to `rememo/otp/memo`. + +## v1.0.0 -- 24-04-16 + +* Initial release. diff --git a/README.md b/README.md index 56af8ef..b8bc056 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ There is some overhead to sending messages and caching the dictionary of results gleam add rememo ``` ```gleam -import rememo +import memo/ets/memo // This is the recommended implementation to use import gleam/io pub fn main() { // Start the actor that holds the cached values // for the duration of this block - use cache <- rememo.create() + use cache <- memo.create() fib(300, cache) |> io.debug } @@ -25,7 +25,7 @@ pub fn main() { fn fib(n, cache) { // Check if a value exists for the key n // Use it if it exists, update the cache if it doesn't - use <- rememo.memoize(cache, n) + use <- memo.memoize(cache, n) case n { 1 | 2 -> 1 n -> fib(n - 1, cache) + fib(n - 2, cache) diff --git a/gleam.toml b/gleam.toml index a6a4837..3c1fb4f 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "rememo" -version = "1.0.0" +version = "2.0.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. @@ -16,6 +16,8 @@ repository = { type = "github", user = "hunkyjimpjorps", repo = "rememo" } gleam_stdlib = "~> 0.34 or ~> 1.0" gleam_otp = "~> 0.10" gleam_erlang = "~> 0.25" +carpenter = ">= 0.3.1 and < 1.0.0" +youid = ">= 1.0.0 and < 2.0.0" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index 5f7e5eb..2ce6690 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,14 +2,19 @@ # You typically do not need to edit this file packages = [ + { name = "carpenter", version = "0.3.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "carpenter", source = "hex", outer_checksum = "7F5AF15A315CF32E8EDD0700BC1E6711618F8049AFE66DFCE82D1161B33F7F1B" }, + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, + { name = "youid", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "youid", source = "hex", outer_checksum = "D0ECB8C57449866A1B1A83964EEEA72160BF8E05F8F2EEA540BD0F1626FA94E2" }, ] [requirements] +carpenter = { version = ">= 0.3.1 and < 1.0.0" } gleam_erlang = { version = "~> 0.25" } gleam_otp = { version = "~> 0.10" } gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } +youid = { version = ">= 1.0.0 and < 2.0.0"} diff --git a/src/rememo/ets/memo.gleam b/src/rememo/ets/memo.gleam new file mode 100644 index 0000000..ac1eedf --- /dev/null +++ b/src/rememo/ets/memo.gleam @@ -0,0 +1,66 @@ +//// This is the memoization implementation that uses [Erlang Term Storage](https://www.erlang.org/doc/man/ets.html) (ETS). +//// This is the faster (and newer) of the two implementations. + +import carpenter/table.{type Set, AutoWriteConcurrency, Private} +import youid/uuid + +/// Start an actor that holds a memoization cache. Pass this cache to the +/// function you want to memoize. +/// This is best used with a `use` expression: +/// ```gleam +/// use cache <- create() +/// f(a, b, c, cache) +/// ``` +/// +pub fn create(apply fun: fn(Set(k, v)) -> t) { + let table_name = uuid.v4_string() + + let assert Ok(cache_table) = + table.build(table_name) + |> table.privacy(Private) + |> table.write_concurrency(AutoWriteConcurrency) + |> table.read_concurrency(True) + |> table.decentralized_counters(True) + |> table.compression(False) + |> table.set() + + let result = fun(cache_table) + table.drop(cache_table) + result +} + +/// Manually add a key-value pair to the memoization cache. +pub fn set(in cache: Set(k, v), for key: k, insert value: v) -> Nil { + table.insert(cache, [#(key, value)]) +} + +/// Manually look up a value from the memoization cache for a given key. +pub fn get(from cache: Set(k, v), fetch key: k) -> Result(v, Nil) { + case table.lookup(cache, key) { + [] -> Error(Nil) + [#(_, v), ..] -> Ok(v) + } +} + +/// Look up the value associated with the given key in the memoization cache, +/// and return it if it exists. If it doesn't exist, evaluate the callback function +/// and update the cache with the value it returns. +/// +/// This works well with a `use` expression: +/// ```gleam +/// fn f(a, b, c, cache) { +/// use <- memoize(cache, #(a, b, c)) +/// // function body goes here +/// } +/// ``` +/// +pub fn memoize(with cache: Set(k, v), this key: k, apply fun: fn() -> v) -> v { + case get(from: cache, fetch: key) { + Ok(value) -> value + Error(Nil) -> { + let result = fun() + set(in: cache, for: key, insert: result) + result + } + } +} diff --git a/src/rememo.gleam b/src/rememo/otp/memo.gleam similarity index 92% rename from src/rememo.gleam rename to src/rememo/otp/memo.gleam index 0017b31..46d6d4d 100644 --- a/src/rememo.gleam +++ b/src/rememo/otp/memo.gleam @@ -1,7 +1,10 @@ +//// This is the memoization implementation that uses [`gleam/otp/actor`](https://www.erlang.org/doc/man/ets.html). +//// This is the slower (and original) of the two implementations. + import gleam/dict.{type Dict} -import gleam/otp/actor.{type Next, Continue, Stop} import gleam/erlang/process.{type Subject, Normal} import gleam/option.{None} +import gleam/otp/actor.{type Next, Continue, Stop} const timeout = 1000