Packages

Lazily initialised constants for Gleam

Current section

Files

Jump to
lazy_const src lazy_const.gleam
Raw

src/lazy_const.gleam

//// 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](https://www.erlang.org/doc/apps/erts/persistent_term.html)
//// in Erlang, and using a global
//// [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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`:
////
//// ```gleam
//// 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.
/// A type used to make sure you don't accidentily pass the constructor and
/// wrapper functions the wrong order!
pub opaque type Id(a) {
Id(inner: fn() -> 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 defined_in(outer wrapper_function: fn() -> a) -> Id(a) {
Id(wrapper_function)
}
/// 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.
pub fn new(in wrapper_function: Id(a), ctor inner_fn: fn() -> a) -> a {
do_new(wrapper_function.inner, inner_fn)
}
@external(erlang, "lazy_const_ffi", "new")
@external(javascript, "./lazy_const_ffi.mjs", "default")
fn do_new(_outer: fn() -> a, inner: fn() -> a) -> a {
inner()
}