Version License Downloads Recent downloads CI status

michie (sounds like Mickey) — an attribute macro that adds [memoization] to a function.

Table of contents

  1. Features
  2. Non-features
  3. key_expr
  4. store_type
  5. store_init
  6. Type requirements
    1. General bounds
    2. Store bounds
  7. Generic functions
  8. Functions that take no input
  9. How it works
  10. Why must key_expr be provided
  11. Support and feedback
  12. Authored by Mobus Operandi

Features

Non-features

key_expr

In each invocation a key is obtained. It is used to query the function's cache store for a possible hit. An expression that evaluates into a key must be provided via the key_expr argument. The expression may use bindings from the function's parameters. In the following example the key_expr is simply the name of the only parameter.

```rust use michie::memoized; use std::collections::HashMap;

[memoized(keyexpr = input, storetype = HashMap)]

fn f(input: usize) -> usize { // expensive calculation # unimplemented!() } ```

store_type

A concrete store type must be either provided via the store_type argument or inferred from the store_init (next section).

The provided type must implement [MemoizationStore]. Implementations of [MemoizationStore] for [BTreeMap] and [HashMap] are provided. In the following example, [BTreeMap] is provided as the store:

```rust use michie::memoized; use std::collections::BTreeMap;

[memoized(keyexpr = input, storetype = BTreeMap)]

fn f(input: usize) -> usize { // expensive calculation # unimplemented!() } ```

store_init

By default, the store is initialized via [Default::default()]. Different initialization may be provided via an expression to store_init:

```rust use michie::{memoized, MemoizationStore}; use std::collections::HashMap;

[memoized(keyexpr = input, storeinit = HashMap::::with_capacity(500))]

fn f(input: usize) -> usize { // expensive calculation # unimplemented!() } ```

Type requirements

Bounds apply to the key type and the function's return type. Some are from the general instrumentation and others are via the store type's implementation of [MemoizationStore].

General bounds

The following apply to the key type and to the function's return type:

Store bounds

Another source of bounds on the key type and the return type is the implementation of [MemoizationStore] for the store type.

Generic functions

Be mindful of the type requirements when using on a generic function:

```rust use michie::memoized; use std::hash::Hash; use std::collections::HashMap;

[memoized(keyexpr = input.clone(), storetype = HashMap)]

fn f(input: A) -> B where A: 'static + Send + Sync // bounds from instrumentation + Eq + Hash // store-specific bounds + Clone, // used in this function's key_expr B: 'static + Send + Sync + Clone // bounds from instrumentation + From, // used in this function's body { input.into() } ```

Functions that take no input

Functions that take no input are good candidates for [compile-time evaluation], which is usually preferred over runtime caching (such as this crate provides). Nonetheless, some functions cannot be evaluated at compile time. A reasonable key_expr for a function that takes no input is ():

```rust use michie::memoized; use std::collections::HashMap;

[memoized(keyexpr = (), storetype = HashMap<(), f64>)]

fn f() -> f64 { // expensive calculation # unimplemented!() } ```

How it works

The original function expands into something similar to this:

rust ignore fn f(input: Input) -> Output { static STORE = Mutex::new(#store_init); let key = #key_expr; let store_mutex_guard = STORE.lock().unwrap(); let attempt = store_mutex_guard.get(&key).cloned(); drop(store_mutex_guard); if let Some(hit) = attempt { return hit; } else { let miss = #original_fn_body; STORE.lock().unwrap().insert(key, miss.clone()); return miss; }; }

Why must key_expr be provided

The only conceivable default key_expr is the entire input. For example, for a function signature: text fn f(a: usize, _b: usize) -> usize the default key_expr would be (a, _b). Two potential problems: some parameters might not satisfy bounds on the key type. Also, the resulting key might be a supervalue of the input of the actual calculation. To explain the latter problem, here is an example:

``rust use michie::memoized; use std::collections::HashMap; // pretend thatkey_expr` is omitted and that this is the default

[memoized(keyexpr = (a, _b), storetype = HashMap<(usize, usize), usize>)]

fn f(a: usize, _b: usize) -> usize { // the actual calculation uses a subvalue of the input — only a # a } f(0, 0); // expected miss because it's the first invocation f(0, 1); // avoidable miss! ```

If an accurate key_expr = a had been provided, the second execution would have been a hit. To summarize, key_expr is mandatory in order to encourage proper consideration of it.

Support and feedback

In [the GitHub Discussions].

Authored by Mobus Operandi

This crate is a work by [Mobus Operandi] — a small community for the regular study of Rust in remote mob programming format.