lazy_const

This is a simple library that allows you to define lazily initialised constants, without the usual restrictions around constants in Gleam. Examples where this might be useful include only compiling regexes once, reading environment variables, or caching and re-using options constructed using the builder pattern.

Constants are stored using persistent terms in Erlang, and using a global Map in Javascript. Make sure you read the documentation for your target platform to understand the performance implications.

This library can easily be overused! Only reach for it if you tested and measured that it makes a noticable difference! Usually, constructing constants is very cheap, even if it looks like duplicate work. Both targets already include optimisations to make this faster. In particular, every access of a constant defined using lazy_const also includes the use of a type constructor and a bunch of function calls.

Type Safety

Always create a new top-level wrapper function for each lazy constant. The first line of your constant must always look like this, for a constant named my_constant:

use <- lazy_const.new(lazy_const.defined_in(my_constant))
// ...

The first line must always use lazy_const.new, and must call lazy_const.defined_in, passing the outer wrapping function.

The outer wrapper function passed as the first argument is used as the key inside the cache structure, so a constant is only unique if that key doesn’t change, and there can only ever be one constant per Id argument.

Functions defined at the top-level are guaranteed to be constant values. If a locally defined function is used, a new constant might be created for every call, depending on the target and the currently active optimisations, degrading performance and using up more and more memory on each call.

Example usage

import gleam/regex
// only ever compile this regex once
fn alphanum_re() -> regex.Regex {
  use <- lazy_const.new(lazy_const.defined_in(alphanum_re))
  let assert Ok(re) = regex.from_string("[a-zA-Z0-9]+")
  re
}

// an expensive calculation that should only be done once.
fn fib40() -> Int {
  use <- lazy_const.new(lazy_const.defined_in(fib40))
  fib(40)
}

fn fib(n: Int) -> Int {
  case n {
    1 -> 1
    2 -> 1
    n -> fib(n - 1) + fib(n - 2)
  }
}

Always prefer few big constants over many small ones.

Limitations

You cannot construct cyclical values using this library - calling a constant inside of its definition will still lead to a stack overflow.

Constants have to be defined as top-level functions and cannot take any arguments.

The target (in particular Erlang) may put additional limits on the amount constants defined, and performance may degrade for every additional constant.

While in in most cases, the constructor is only ever called once, this should shoul not be relied upon without additional safety nets - the target is in theory allowed to evict constants at any time. Every node in a cluster will also have its own instance of a constant.

Types

Id

opaque </>

A type used to make sure you don’t accidentily pass the constructor and wrapper functions the wrong order!

pub opaque type Id(a)

Functions

pub fn defined_in(outer wrapper_function: fn() -> a) -> Id(a)

Explicitely mark a function as the wrapping function where the constant is defined in.

Read the module documentation at the top to learn more about how this library works and how to use it!

pub fn new(
  in wrapper_function: Id(a),
  ctor inner_fn: fn() -> a,
) -> a

Define a new lazy constant. The ctor will only be called once, after which the returned value will be cached, such that future invocations are fast!

Warning: Please make sure you have read the docs at the top of the module first.

Search Document