A Rust attribute macro that adds memoization to a function (rhymes with Mickey)

Overview

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. key_type
  5. store_type
  6. store_init
  7. Store inference and the default store
  8. Type requirements
    1. General bounds
    2. Store bounds
  9. Generic functions
  10. Functions that take no input
  11. How it works
  12. Why must key_expr be provided
  13. Support and feedback
  14. Authored by Mobus Operandi

Features

  • Supports
    • Plain functions
    • Generic functions
    • Functions in impl blocks
    • Functions in trait implementation blocks
    • Functions that are default trait implementations
  • Thread safe
  • Expansion depends on only std
  • Hygienic
  • Supports recursive functions
  • Bring your own store

Non-features

  • Caching features: this crate does not provide a caching mechanism other than some trivial implementations. It allows you to bring your own.
  • "Blazingly fast": this crate aims to provide a simple and easy-to-use means of memoizing a function. If you actually really require micro-optimized memoization then you'd most likely have to implement it yourself.

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.

use michie::memoized;
#[memoized(key_expr = input)]
fn f(input: usize) -> usize {
    // expensive calculation
    # unimplemented!()
}

key_type

While the type of the key supports inference, it may also be specified using the key_type argument:

use michie::memoized;
#[memoized(key_type = u64, key_expr = input.into())]
fn f(input: u32) -> u32 {
    // expensive calculation
    # unimplemented!()
}

store_type

A store type may be provided via the store_type argument. 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:

use michie::memoized;
use std::collections::BTreeMap;
#[memoized(key_expr = input, store_type = BTreeMap<usize, usize>)]
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:

use michie::{memoized, MemoizationStore};
use std::collections::HashMap;
#[memoized(key_expr = input, store_init = HashMap::with_capacity(500))]
fn f(input: usize) -> usize {
    // expensive calculation
    # unimplemented!()
}

Store inference and the default store

An omitted store_type may be inferred from a provided store_init. If both are omitted, the default store_type is [HashMap].

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:

  • [Sized]: for one, the instrumentation stores the key in a let binding.
  • 'static: key and return values are owned by a store which is owned by a static.
  • [Send] and [Sync]: for parallel access.

And the return type must be [Clone] because it is cloned for insertion into the store.

Store bounds

Another source of bounds on the key type and the return type is the implementation of [MemoizationStore] for the store type. By the way, the provided implementation of [MemoizationStore] for the default store type [HashMap] bounds K: Eq + Hash.

Generic functions

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

use michie::memoized;
use std::hash::Hash;
#[memoized(key_expr = input.clone())]
fn f<A, B>(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<A>, // 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 ():

use michie::memoized;
#[memoized(key_expr = ())]
fn f() -> f64 {
    // expensive calculation
    # unimplemented!()
}

How it works

The original function expands into something similar to this:

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:

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:

use michie::memoized;
// pretend that `key_expr` is omitted and that this is the default
#[memoized(key_expr = (a, _b))]
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.

You might also like...
Proc macro implementation of #[naked]

#[naked] Documentation This crate provide a proc macro version of the #[naked] attribute which can be used on stable Rust. Example // The SYSV64 calli

Macro for fast implementing serialize methods in serde::Serializer trait

impl_serialize! This library provides a simple procedural macro for fast implementing serialize methods in serde::Serializer trait. [dependencies] imp

hashmap macro for creating hashmap from provided key/value pairs

HashMap Macro Creates a HashMap from provided key/value pairs. Usage use std::collections::HashMap; use hashmap_macro::hashmap; let m: HashMap&str,

Procedural macro to derive Serde serializer-deserializer for Prost

prost-serde-derive prost-serde-derive is a procedural macro to generate Serde serializers and deserializers for Prost-generated structs. Rationale Cur

proc-macro to help with using surrealdb's custom functions

SurrealDB Functions This is a proc-macro crate that given a path to a .surql file or a folder of .surql files, will parse DEFINE FUNCTION fn::s inside

todo2(a.k.a. todo or die) - A better todo! macro inspired from searls/todo_or_die

todo2 todo2(a.k.a. todo or die) - A better todo! macro inspired from searls/todo_or_die This crate provides a better todo! macro, which allows you to

Leetcode Solutions in Rust, Advent of Code Solutions in Rust and more

RUST GYM Rust Solutions Leetcode Solutions in Rust AdventOfCode Solutions in Rust This project demostrates how to create Data Structures and to implem

Simple autoclicker written in Rust, to learn the Rust language.

RClicker is an autoclicker written in Rust, written to learn more about the Rust programming language. RClicker was was written by me to learn more ab

Rust programs written entirely in Rust

mustang Programs written entirely in Rust Mustang is a system for building programs built entirely in Rust, meaning they do not depend on any part of

Comments
  • RE: `--from-reference` doesn't seem to work in GitHub Actions

    RE: `--from-reference` doesn't seem to work in GitHub Actions

    I responded to the issue that was opened by a maintainer/contributor of this tool.

    https://gitlab.com/DeveloperC/conventional_commits_linter/-/issues/1

    I know GitLab is terrible with notifications so just prompting here, feel free to close this issue and the one on GitLab if you are satisfied.

    opened by DeveloperC286 1
  • feat: remove key_type parameter

    feat: remove key_type parameter

    opened by mightyiam 0
  • maintenance

    maintenance

    • test: underscore some intentionally unused fns
    • test: update expected error messages
    • build: upgrade cargo-edit and cargo-make
    • build: upgrade jql
    • build: upgrade cargo_toml
    • build: update sub-dep libc
    • build: do not set RUSTFLAGS in Makefile
    opened by mightyiam 0
  • implement support for `cached`-style result and option attributes

    implement support for `cached`-style result and option attributes

    This PR implements support for result and option attributes, where the cache is instructed to only cache the success case of a function returning Result<T,E> or Option<T>. This mimics the interface of cached, but if you would prefer a different interface, that's fine by me.

    opened by SohumB 12
Releases(v3.0.0)
Owner
Mobus Operandi
Mobus Operandi
Attribute for defining `macro_rules!` macros with proper visibility and scoping

macro-vis This crate provides an attribute for defining macro_rules! macros that have proper visibility and scoping. The default scoping and publicity

null 2 Aug 29, 2022
a function programming language for real world applications made in rust

a function programming language for real world applications made in rust

Tanay Pingalkar 6 Jun 12, 2022
A Quest to Find a Highly Compressed Emoji :shortcode: Lookup Function

Highly Compressed Emoji Shortcode Mapping An experiment to try and find a highly compressed representation of the entire unicode shortcodes-to-emoji m

Daniel Prilik 13 Nov 16, 2021
Allow function lifetime elision and explicit `for<'a>` annotations on closures.

::higher-order-closure Allow function lifetime elision and explicit for<'a> annotations on closures. Motivation / Rationale See the RFC #3216: this cr

Daniel Henry-Mantilla 18 Dec 26, 2022
Tool to convert variable and function names in C/C++ source code to snake_case

FixNameCase Tool to convert variable and function names in C/C++ source code to snake_case. Hidden files and files listed in .gitignore are untouched.

AgriConnect 4 May 25, 2023
A Rust macro for writing nested loop expressions

loop_chain A Rust macro for writing nested loop expressions Usage | Examples | Docs Dependencies [dependencies] loop_chain = "0.1.1" Usage For express

Takayuki Maeda 5 Jul 30, 2021
Macro assembler for Rust

Macro Assembler This crate implement JSC/SpiderMonkey like macro assembler. Macro assembler purpose is to generate machine code for different platform

playX 6 Nov 26, 2022
Library and proc macro to analyze memory usage of data structures in rust.

Allocative: memory profiler for Rust This crate implements a lightweight memory profiler which allows object traversal and memory size introspection.

Meta Experimental 19 Jan 6, 2023
Simple Rust derive-macro that simplifies integral enum creation

Integral enum A simple way to define integer-like enums. This macro implements bunch of traits that are usually implemented via looooong derive(..) at

null 3 Jan 6, 2023
A procedural macro for configuring constant values across crates

toml-cfg Rough ideas: Crates can declare variables that can be overridden Anything const, e.g. usize, strings, etc. (Only) The "root crate" can overri

James Munns 43 Dec 24, 2022