{"id":27407223,"url":"https://github.com/roklenarcic/memento","last_synced_at":"2025-04-14T07:10:55.872Z","repository":{"id":53494862,"uuid":"284541395","full_name":"RokLenarcic/memento","owner":"RokLenarcic","description":"Clojure Memoization project","archived":false,"fork":false,"pushed_at":"2024-10-28T14:14:04.000Z","size":636,"stargazers_count":33,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-13T23:40:06.346Z","etag":null,"topics":["clojure","memoization","memoize"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RokLenarcic.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-08-02T21:04:24.000Z","updated_at":"2025-04-07T13:32:25.000Z","dependencies_parsed_at":"2024-03-11T10:29:11.879Z","dependency_job_id":"40117d2f-a88f-4be2-a7f7-c3a856083f73","html_url":"https://github.com/RokLenarcic/memento","commit_stats":{"total_commits":46,"total_committers":1,"mean_commits":46.0,"dds":0.0,"last_synced_commit":"47b5129048d0cf9cad935a731cdd1b9c59101f48"},"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RokLenarcic%2Fmemento","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RokLenarcic%2Fmemento/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RokLenarcic%2Fmemento/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RokLenarcic%2Fmemento/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RokLenarcic","download_url":"https://codeload.github.com/RokLenarcic/memento/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248837287,"owners_count":21169374,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","memoization","memoize"],"created_at":"2025-04-14T07:10:55.226Z","updated_at":"2025-04-14T07:10:55.863Z","avatar_url":"https://github.com/RokLenarcic.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Memento\n\nA library for function memoization with scoped caches and tagged eviction capabilities.\n\n## Dependency\n\n[![Clojars Project](https://img.shields.io/clojars/v/org.clojars.roklenarcic/memento.svg)](https://clojars.org/org.clojars.roklenarcic/memento)\n\n## Version 2.0 breaking changes\n\nVersion 2 moves from Java 8 to Java 11 as minimum JVM version. Caffeine version is 3 instead of 2.\n\n## Version 1.0 breaking changes\n\nVersion 1.0 represents a switch from Guava to Caffeine, which is a faster caching library, with added\nbenefit of not pulling in the whole Guava artefact which is more that just that Cache. The Guava Cache type\nkey and the config namespace are deprecated and will be removed in the future.\n\n## Motivation\n\nWhy is there a need for another caching library?\n\n- request scoped caching (and other scoped caching)\n- eviction by secondary index\n- disabling cache for specific function returns\n- tiered caching\n- size based eviction that puts limits around more than one function at the time\n- cache events\n\n## Performance\n\n- [**Performance**](doc/performance.md)\n\n## Adding cache to a function\n\n**With require `[memento.core :as m][memento.config :as mc]`:**\n\nDefine a function + create new cache + attach cache to a function:\n\n```clojure\n(m/defmemo my-function\n  {::m/cache {mc/type mc/caffeine}}\n  [x]\n  (* 2 x))\n```\n\n### **The key parts here**:\n- `defmemo` works just like `defn` but wraps the function in a cache \n- specify the cache configuration via `:memento.core/cache` keyword in function meta\n\nQuick reminder, there are two ways to provide metadata when defining functions: `defn` allows a meta\nmap to be provided before the argument list, or you can add meta to the symbol directly as supported by the reader:\n\n```clojure\n(m/defmemo ^{::m/cache {mc/type mc/caffeine}} my-function\n  [x]\n  (* 2 x))\n```\n\n### Caching an anonymous function\n\nYou can add cache to a function object (in `clojure.core/memoize` fashion):\n\n```clojure\n(m/memo (fn [] ...) {mc/type mc/caffeine})\n```\n\n### Other ways to attach Cache to a function\n\n[Caches and memoize calls](doc/major.md)\n\n## Cache conf(iguration)\n\nSee above: `{mc/type mc/caffeine}`\n\nThe cache conf is an open map of namespaced keywords such as `:memento.core/type`, various cache implementations can\nuse implementation specific config keywords.\n\nLearning all the keywords and what they do can be hard. To assist you \nthere are special conf namespaces provided where conf keywords are defined as vars with docs,\nso it's easy so you to see which configuration keys are available and what their function is. It also helps\nprevent bugs from typing errors.\n\nThe core properties are defined in `[memento.config :as mc]` namespace. Caffeine specific properties are defined\nin `[memento.caffeine.config :as mcc]`. \n\nHere's a couple of equal ways of writing out you cache configuration meta:\n\n```clojure\n; the longest\n{:memento.core/cache {:memento.core/type :memento.core/caffeine}}\n; using alias\n{::m/cache {::m/type ::m/caffeine}}\n; using memento.config vars - recommended\n{mc/cache {mc/type mc/caffeine}}\n```\n\n### Core conf\n\nThe core configuration properties:\n\n#### mc/type\n\nCache implementation type, e.g. caffeine, redis, see the implementation library docs. **Make sure\nyou load the implementation namespace at some point!**. Caffeine namespace is loaded automatically\nwhen memento.core is loaded.\n\n#### mc/size\u003c\n\nSize limit expressed in number of entries or total weight if implementation supports weighted cache entries\n\n#### mc/ttl\n\nEntry is invalid after this amount of time has passed since its creation\n\nIt's either a number (of seconds), a pair describing duration e.g. `[10 :m]` for 10 minutes,\nsee `memento.config/timeunits` for timeunits.\n\n#### mc/fade\n\nEntry is invalid after this amount of time has passed since last access, see `mc/ttl` for duration\nspecification.\n\n#### mc/key-fn, mc/key-fn*\n\nSpecify a function that will transform the function arg list into the final cache key. Used \nto drop function arguments that shouldn't factor into cache tag equality.\n\nThe `key-fn` receives a sequence of arguments, `key-fn*` receives multiple arguments as if it\nwas the function itself.\n\nSee: [Changing the key for cached tag](doc/key-fn.md)\n\n#### mc/ret-fn\n\nA function that is called on every cached function return value. Used for general transformations\nof return values.\n\n#### mc/ret-ex-fn\n\nA function that is called on every thrown Throwable. Used for general transformations\nof thrown exceptions values.\n\n#### mc/seed\n\nInitial entries to load in the cache.\n\n#### mc/initial-capacity\n\nCache capacity hint to implementation.\n\n## Conf is a value (map)\n\nCache conf can get quite involved:\n\n```clojure\n(ns memento.tryout\n  (:require [memento.core :as m]\n    ; general cache conf keys\n            [memento.config :as mc]\n    ; caffeine specific cache conf keys\n            [memento.caffeine.config :as mcc]))\n\n(def my-weird-cache\n  \"Conf for caffeine cache that caches up to 20 seconds and up to 30 entries, uses weak\n  references and prints when keys get evicted.\"\n  {mc/type mc/caffeine\n   mc/size\u003c 30\n   mc/ttl 20\n   mcc/weak-values true\n   mcc/removal-listener #(println (apply format \"Function %s key %s, value %s got evicted because of %s\" %\u0026))})\n\n(m/defmemo my-function\n  {::m/cache my-weird-cache}\n  [x] (* 2 x))\n```\n\nSeeing as cache conf is a map, I recommend a pattern where you have a namespace in your application that contains vars\nwith your commonly used cache conf maps and functions that generate slightly parameterized\nconfiguration. E.g.\n\n```clojure\n(ns my-project.cache\n  (:require [memento.config :as mc]))\n\n;; infinite cache\n(def inf-cache {mc/type mc/caffeine})\n\n(defn for-seconds [n] (assoc inf-cache mc/ttl n))\n```\n\nThen you just use that in your code:\n\n```clojure\n(m/defmemo my-function\n  {::m/cache (cache/for-seconds 60)}\n  [x] (* x 2))\n```\n\n## Caches and mount points\n\nEnabling memoization of a function is composed of two distinct steps:\n\n- creating a Cache (optional, as you can use an existing cache)\n- binding the cache to the function (a MountPoint is used to connect a function being memoized to the cache)\n\nA cache, an instance of memento.base/Cache, can contain entries from multiple functions and can be shared between memoized functions.\nEach memoized function is bound to a Cache via MountPoint. When you call a function such as `(m/as-map a-cached-function)` you are \noperating on a MountPoint.\n\nThe reason for this separation is two-fold:\n\n#### 1. **Improved Size Based Eviction**\n\nSo far all examples implicitly created a new cache for each memoized function, but if we use same cache for multiple \nfunctions, then any size based eviction will apply to them as a whole. If you have 100 memoized functions, and you want to\nsomewhat limit their memory use, what do you do? In a typical cache library you might limit each of them to 100 entries. So you\nallocated 10000 slots total, but one function might have an empty cache, while a very heavily used one needs way more than 100\nslots. If all 100 function are backed by same Cache instance with 10000 slots then they automatically balance themselves out.\n\n#### 2. **Changing cache temporarily to allow for scoped caching**\n\nThis indirection with Mount Points allows us to change which cache is backing a function dynamically. See discussion of tagged\ncaches below. Here's an example of using tags when caching and scoped caching\n\n```clojure\n(ns myproject.some-ns\n  (:require [myproject.cache :as cache]\n            [memento.core :as m]))\n\n(defn get-person-by-id [person-id]\n  (let [person (db/get-person person-id)]\n    ; tag the returned object with :person + id pair\n    (m/with-tag-id person :person (:id person))))\n\n; add a cache to the function with tags :person and :request\n(m/memo #'get-person-by-id [:person :request] cache/inf)\n\n; remove cache entries from every cache tagged :person globally, where the\n; tag is tagged with :person 1\n(m/memo-clear-tag! :person 1)\n\n(m/with-caches :request (constantly (m/create cache/inf))\n  ; inside this block, a fresh new cache is used (and discarded)\n  ; making a scope-like functionality\n  (get-person-by-id 5))\n```\n\n## Variable expiry\n\nInstead of setting a fixed duration of validity for entries in a cache, it is possible\nto set these duration on per-tag or per-mount point basis.\n\nNote that for Caffeine cache variable expiry caching is somewhat slower.\n\n### **Read [here](doc/variable-expiry.md)**\n\n## Additional features\n\n#### [Prevent caching of a specific return value (and general return value xform)](doc/ret-fn.md)\n#### [Manually add or evict entries](doc/manual-add-remove.md)\n\n#### `(m/as-map memoized-function)` to get a map of cache entries, also works on MountPoint instances\n#### `(m/memoized? a-function)` returns true if the function is memoized\n#### `(m/memo-unwrap memoized-function)` returns original uncached function, also works on MountPoint instances\n#### `(m/active-cache memoized-function)` returns Cache instance from the function, if present.\n\n## Tags \n\nYou can add tags to the caches. Tags enable that you:\n\n- run actions on caches with specific tags\n- **change or update cache of tagged MountPoints within a scope**\n- change or update cache of tagged MountPoints permanently\n- use secondary index to invalidate entries by a tag + ID pair\n\nThis is a very powerful feature, [read more here.](doc/tags.md)\n\n## Loads and invalidations\n\nCache only has a single ongoing load for a key going at any one time. For Caffeine cache, if a key is invalidated\nduring the load, the load is repeated. This is the only way you can get multiple function invocations happen for a single\ncached function call. When an tag is invalidated while it's being loaded, the Thread that loads it will be interrupted.\n\n## Namespace scan\n\nYou can scan loaded namespaces for annotated vars and automatically create caches.\n\n[Read more](doc/ns-scan.md)\n\n## Events\n\nYou can fire an event at a memoized function. Main use case is to enable adding entries to different functions from same data.\n\n[Read more](doc/events.md)\n\n## Tiered caching\n\nYou can use caches that combine two other caches in some way. The easiest way to generate\nthe cache configuration needed is to use `memento.core/tiered`,`memento.core/consulting`, `memento.core/daisy`.\n\n[Read more](doc/tiered.md)\n\n## if-cached\n\nmemento.core/if-cache is like an if-let, but the \"then\" branch executes if the function call\nis cached, otherwise else branch is executed. The binding is expected to be a cached function call form, otherwise \nan error is thrown. \n\nExample:\n\n```clojure\n(if-cached [v (my-function arg1)]\n  (println \"cached value is \" v)\n  (println \"value is not cached\"))\n```\n\n## Skip/disable caching\n\nIf you set `-Dmemento.enabled=false` JVM option (or change `memento.config/enabled?` var root binding), \nthen type of all caches created will be `memento.base/no-cache`, which does no caching. \n\n## Reload guards\n\nWhen you memoize a function with tags, a special object is created that will clean up in internal tag\nmappings when memoized function is GCed. It's important when reloading namespaces to remove mount points\non the old function versions.\n\nIt uses finalize, which isn't free (takes extra work to allocate and GC has to work harder), so\nif you don't use namespace reloading, and you want to optimize you can disable reload guard objects.\n\nSet `-Dmemento.reloadable=false` JVM option (or change `memento.config/reload-guards?` var root binding).\n\n## Breaking changes\n\nPatch versions are compatible. Minor version change breaks API for implementation authors, but not for users,\nmajor version change breaks user API.\n\nVersion 1.0.x changed implementation from Guava to Caffeine\nVersion 0.9.0 introduced many breaking changes.\n\n## License\n\nCopyright © 2020-2021 Rok Lenarčič\n\nLicensed under the term of the MIT License, see LICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froklenarcic%2Fmemento","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froklenarcic%2Fmemento","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froklenarcic%2Fmemento/lists"}