StaticMap
A compile-time macro to build a module that behaves like a map()
, but with “pre-baked” lookup functions.
Installation
Add static_map
to your list of dependencies in mix.exs
:
def deps do
[
{:static_map, "~> 0.1.1"}
]
end
Usage
First, require and import StaticMap
into the current context:
use StaticMap
Then you can define a map module:
defmap MyMap, [
a: 1,
b: 2
]
The first argument to StaticMap.defmap/2
is the name of the module to define, as an Alias (calling defmap Bar, ...
from within the body of a module Foo
will thus define Foo.Bar
.)
You can pass any enumerable value as the second argument; it will be transformed into a map()
using Map.new/1
.
The module defined above (MyMap
) will contain the following functions:
MyMap.to_map/0
: returns the mapMyMap.to_list/0
: returns the map, passed throughEnum.to_list/1
MyMap.get/1
: behaves asMap.get/2
MyMap.get/2
: behaves asMap.get/3
MyMap.fetch/1
: behaves asMap.fetch/2
MyMap.fetch!/1
: behaves asMap.fetch!/2
MyMap.has_key?/1
: behaves asMap.has_key?/2
MyMap.keys_set/0
: behaves asMap.keys/1
, passed throughMapSet.new/1
MyMap.values_set/0
: behaves asMap.values/1
, passed throughMapSet.new/1
None of the above functions have any runtime logic; all return values are generated at compile time, and each potential input is built into its own function clause.
Precompiled accessor macros
StaticMap
contains macros that allow for compile-time lookups of values in your map module.
These macros act the same as their equivalent functions in Map
, taking your map module in place of a map()
.
When used in your code, these macros will—if possible—expand to their literal value, rather than to a function-call to your map module. (See the Efficiency Guide below for more details.) These macro-accessor calls can effectively replace the usage of huge numbers of literals or scalar module attributes in your code:
# Before
@foo_one 1
@foo_two 2
def x, do: [
@foo_one,
@foo_two
]
# After
defmap Foo, %{one: 1, two: 2}
def x, do: [
StaticMap.fetch!(Foo, :one),
StaticMap.fetch!(Foo, :two)
]
If literal expansion is not possible (e.g. if you are passing a variable key name to the macro-call), calls to the accessor macros will instead expand to calls to your map-module’s accessor functions. Thus, there are no disadvantages (other than verbosity) to always preferring the macro-accessors on StaticMap
over directly calling the accessor functions on the map-module.
Efficiency Guide
Runtime clause-match overhead
Because map-key hashing has a constant overhead, the accessor functions defined on the map module (MyMap.has_key?/1
, MyMap.get/1,2
, MyMap.fetch/1
, and MyMap.fetch!/1
) will be faster than their counterparts in Map
, but only up to a point. For large (>500 pair) maps, the O(log n)
time-complexity of the binary-search op used in clause-head unification will outweigh the constant overhead of map-key hashing.
For such high-cardinality maps, it is better to rely on StaticMap
only for its value functions (MyMap.to_map/0
, MyMap.to_list/0
, MyMap.keys_set/0
and MyMap.values_set/0
), and to do any accessing of the map by calling MyMap.to_map/0
and passing the return value to regular Map
functions.
When precompiled accessors will expand to literals
The StaticMap
precompiled-accessor macros first test the passed (quoted) expressions to determine whether they seem to contain only compile-time-available literals. Importantly, any use of a variable or module-attribute in the accessor macro-call will disqualify the macro-call from literal expansion, instead directly expanding to the runtime-call form.
If the passed expressions are eligible for compile-time evaluation, the macro will then attempt to evaluate the passed expressions in the calling context and use the results to perform the lookup. If, during this step, a CompileError
is generated, the macro will fall back to runtime expansion.
The accessor macro has no way of knowing whether a function call is “pure”, and so will happily evaluate and reduce impure/nondeterministic functions to literals. Avoid using nondeterministic functions!
To summarize, here are the guidelines for keys:
- Do: use a literal key —
MyMap |> StaticMap.fetch!(:a)
- Do: use an alias key —
MyMap |> StaticMap.fetch!(Foo)
- Do: pass a literal expression through a pure+deterministic function that evaluates to a literal or alias key —
MyMap |> StaticMap.fetch!(List.first([:a, :b]))
- Do not: use a variable key —
MyMap |> StaticMap.fetch(a)
- Do not: use a module-attribute key —
MyMap |> StaticMap.fetch(@a)
- Do not: pass an expression through an impure/nondeterministic function —
MyMap |> StaticMap.fetch!(make_ref())
- Do not: use a variable or module-attribute in an expression that evaluates to a key —
MyMap |> StaticMap.fetch(List.first([:a, x, @y]))
And here are the guidelines for map-modules:
- Do: use a literal map-module —
:my_map |> StaticMap.fetch!(:a)
- Do: use an alias map-module —
MyMap |> StaticMap.fetch!(:a)
- Do: pass a literal expression through a pure+deterministic function that evaluates to a map-module literal or alias —
[MapA, MapB] |> List.first() |> StaticMap.fetch!(:a)
- Do not: use a module-attribute map-module —
@map_module |> StaticMap.fetch(:a)
- Do not: use a map-module alias that does not name a compiled-and-loaded module —
MapDefinedLaterInTheFile |> StaticMap.fetch!(:a)
- Do not: pass an expression through an impure/nondeterministic function —
[MapA, MapB] |> Enum.shuffle() |> List.first() |> StaticMap.fetch!(:a)
- Do not: use a variable or module-attribute in an expression that evaluates to a map-module —
[MyMap, x, @y] |> List.first() |> StaticMap.fetch(:a)