Liking cljdoc? Tell your friends :D

Memento

A library for function memoization.

Dependency

Clojars Project

Motivation

Why is there a need for another caching library? Motivation here.

Usage

With require as [memento.core :as m]:

You can attach a cache to the function, by wraping it in a memo call:

(def my-function
  (m/memo #(* 2 %)))

This matches a basic memoize.

You can specify cache properties:

(def my-function
  (m/memo {:size< 30 :ttl 40} #(* 2 %)))

This creates a Guava backed cache that is limited to 30 entries and entries expire after 40 seconds.

Read more about specifying properties HERE.

The default implementation is Guava based, and the supported properties are described HERE

You can add cache directly to a var:

(defn my-function [x] (* 2 x))
 
(m/memo {:size< 30 :ttl 40} #'my-function)

If you are using the same settings many times, simply use a variable.

; 1 day cache
(def long-cache {:ttl [1 :d]})
 
(m/memo long-cache #'my-function)

Regions

Normally cache only contains entries pertaining to one function. This presents a problem when you're trying to limit the size of the whole caching mechanism or when trying to invalidate large chunks of cached data in the system.

If you have a 100 cached functions, each with :size< 100, you allow for 10000 cached items, but one function might need 1000 items and another one might never need more than 10. In order to make sizing easier, you can use a cache region.

A Region is a Cache that is shared between many functions and it is named (usually by a keyword).

You can specify that the function uses Region with id :my-region as a cache like this:

(m/memo :my-region #'my-function)

This is equivalent to:

(m/memo {:region :my-region} #'my-function)

When using the map form, you can also specify key-fn, ret-fn, seed.

If you specify a region that doesn't exist then no caching is done until the region is registered:

(set-region! :my-region {:size< 100})

All the functions caching into this region share the same limit of 100 entries.

See more info about regions HERE.

Scoped regions

You define new regions that are active only within a scope (or alias an existing region).

This is extremely useful (and basically the main reason for this library), e.g.:

(m/memo {:region :request-scope} #'my-function)

(with-region :request-scope {}
  ; executes the code with all the :request-scope region functions being cached in a new
  ; region with spec `{}` (infinite cache)
  ...)
; drops all the cached values, as the region created for the scope is dropped

You can also redirect caching to an existing region with region aliasing:

; cache all functions that use :request-scope region in :small-cache region
(with-region-alias :request-scope :small-cache
  ....)

See more info about scopes and regions HERE.

Namespace scan

You can scan loaded namespaces for annotated vars and automatically create caches. The scan looks for Vars with :memento.core/cache key in the meta. That value is used as a cache spec.

Given require [memento.ns-scan :as ns-scan]:

; defn already has a nice way for adding meta
(defn test1
  "A function using built-in defn meta mechanism to specify a cache region"
  {::m/cache :request-scope}
  [arg1 arg2]
  (+ arg1 arg2))

; you can also do standard meta syntax
(defn ^{::m/cache {:size< 100}} test2
  "A function using normal meta syntax to add a cache to itself"
  [arg1 arg2] (+ arg1 arg2))

; this also works on def
(def ^{::m/cache {:size< 100}} test3 (fn [arg1 arg2] (+ arg1 arg2))

; attach caches
(ns-scan/attach-caches)

This only works on LOADED namespaces, so beware.

Calling attach-caches multiple times attaches new caches.

Namespaces clojure.* and nrepl.* are not scanned by default, but you can provide your own blacklists, see doc.

Skip/disable caching

If the wrapped function (or :ret-fn post-processing hook) returns an object wrapped in m/non-cached function, then the result is not cached.

(def call-service
  (m/memo
    {:ttl 10}
    (fn [....] ...
      ; don't cache error responses, but still return them
      (if (<= 400 (:status resp)) (m/non-cached resp) resp))))

or more commonly via, the :ret-fn

(def call-service
  (m/memo
    {:ttl 10
     :ret-fn #(if (<= 400 (:status %)) (m/non-cached %) %)}
    (fn [....] ... resp)))

If you set -Dmemento.enabled=false JVM option, then none of the caches will be used, (though they will still exist, just empty).

Manual eviction

You can manually evict entries:

; invalidate everything
(m/memo-clear! memoized-function)
; invaliate an arg-list
(m/memo-clear! memoized-function arg1 arg2 ...)

You can manually evict all entries in a region:

(m/memo-clear-region! :my-region)

Manually adding entries

You can add entries to a function's cache at any time:

(m/memo-add! memoized-function {[arg1 arg2] result})

Additional utility

  • (m/as-map memoized-function) -> map of cache entries
  • (m/region-as-map :my-region) -> map of region's cache entries
  • (m/memoized? a-function) -> returns true if the function is memoized
  • (m/memo-unwrap memoized-function) -> returns memento.base/Cache object on function if there is one

Developer information

License

Copyright © 2020 Rok Lenarčič

Licensed under the term of the Eclipse Public License - v 2.0, see LICENSE.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close