hb_maps.erl - Link-Resolving Map Operations
Overview
Purpose: Map operations with automatic link resolution
Module: hb_maps
Pattern: Standard maps API + link loading from cache
Warning: Low-level module - use hb_ao for most cases
This module provides a drop-in replacement for Erlang's maps module with automatic resolution of links as they are encountered. It transparently loads linked values from cache, supporting both TABM-encoded link format (Key+link) and structured link tuples.
Critical Warning
⚠️ USE WITH EXTREME CAUTION ⚠️
This module is extremely low-level and should rarely be used directly. In virtually all circumstances, use hb_ao:resolve/3 or hb_ao:get/3 instead, as they execute the full AO-Core protocol:
- Normalize keys
- Apply device functions
- Resolve links
- Type conversions
- Message is in
~message@1.0format - You understand link resolution implications
- You don't need device-specific processing
- Performance is critical enough to skip AO-Core
hb_ao instead!
Core Difference from maps
Standard maps module:
Map = #{key => {link, ID, #{}}},
Value = maps:get(key, Map).
% Result: {link, ID, #{}}hb_maps module:
Map = #{key => {link, ID, #{}}},
Value = hb_maps:get(key, Map).
% Result: <actual value loaded from cache>Dependencies
- HyperBEAM:
hb_cache - Erlang/OTP:
maps - Testing:
eunit
Public Functions Overview
%% Basic Operations
-spec get(Key, Map) -> Value.
-spec get(Key, Map, Default) -> Value.
-spec get(Key, Map, Default, Opts) -> Value.
-spec find(Key, Map) -> {ok, Value} | error.
-spec find(Key, Map, Opts) -> {ok, Value} | error.
-spec put(Key, Value, Map) -> Map.
-spec put(Key, Value, Map, Opts) -> Map.
%% Queries
-spec is_key(Key, Map) -> boolean().
-spec is_key(Key, Map, Opts) -> boolean().
-spec keys(Map) -> [Key].
-spec keys(Map, Opts) -> [Key].
-spec values(Map) -> [Value].
-spec values(Map, Opts) -> [Value].
-spec size(Map) -> non_neg_integer().
-spec size(Map, Opts) -> non_neg_integer().
%% Transformations
-spec map(Fun, Map) -> Map.
-spec map(Fun, Map, Opts) -> Map.
-spec filter(Fun, Map) -> Map.
-spec filter(Fun, Map, Opts) -> Map.
-spec filtermap(Fun, Map) -> Map.
-spec filtermap(Fun, Map, Opts) -> Map.
-spec fold(Fun, Acc, Map) -> Acc.
-spec fold(Fun, Acc, Map, Opts) -> Acc.
%% Set Operations
-spec merge(Map1, Map2) -> Map.
-spec merge(Map1, Map2, Opts) -> Map.
-spec with(Keys, Map) -> Map.
-spec with(Keys, Map, Opts) -> Map.
-spec without(Keys, Map) -> Map.
-spec without(Keys, Map, Opts) -> Map.
-spec remove(Key, Map) -> Map.
-spec remove(Key, Map, Opts) -> Map.
%% Updates
-spec update_with(Key, Fun, Map) -> Map.
-spec update_with(Key, Fun, Map, Opts) -> Map.
-spec take(N, Map) -> Map.
-spec take(N, Map, Opts) -> Map.
%% Conversions
-spec from_list(List) -> Map.
-spec to_list(Map) -> List.
-spec to_list(Map, Opts) -> List.Public Functions
1. get/2, get/3, get/4
-spec get(Key, Map, Default, Opts) -> Value
when
Key :: term(),
Map :: map(),
Default :: term(),
Opts :: map(),
Value :: term().Description: Get value from map, resolving links in both the map itself and the retrieved value.
Link Resolution Order:- Ensure map is loaded (if it's a link)
- Get value at key
- Ensure value is loaded (if it's a link)
-module(hb_maps_get_test).
-include_lib("eunit/include/eunit.hrl").
get_with_link_test() ->
Bin = <<"TEST DATA">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{}}, 3 => 3},
?assertEqual(Bin, hb_maps:get(2, Map)).
get_with_typed_link_test() ->
Bin = <<"123">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{<<"type">> => integer}}, 3 => 3},
?assertEqual(123, hb_maps:get(2, Map, undefined)).
get_default_test() ->
Map = #{key => value},
?assertEqual(default, hb_maps:get(missing, Map, default)).2. find/2, find/3
-spec find(Key, Map, Opts) -> {ok, Value} | error
when
Key :: term(),
Map :: map(),
Opts :: map(),
Value :: term().Description: Search for key in map, resolving links. Returns {ok, Value} if found, error otherwise.
-module(hb_maps_find_test).
-include_lib("eunit/include/eunit.hrl").
find_existing_test() ->
Map = #{key => value},
?assertEqual({ok, value}, hb_maps:find(key, Map)).
find_missing_test() ->
Map = #{key => value},
?assertEqual(error, hb_maps:find(missing, Map)).
find_with_link_test() ->
Data = <<"test">>,
{ok, ID} = hb_cache:write(Data, #{}),
Map = #{key => {link, ID, #{}}},
?assertEqual({ok, Data}, hb_maps:find(key, Map)).3. put/3, put/4
-spec put(Key, Value, Map, Opts) -> Map
when
Key :: term(),
Value :: term(),
Map :: map(),
Opts :: map().Description: Add or update key-value pair in map. Map is loaded before insertion.
Test Code:-module(hb_maps_put_test).
-include_lib("eunit/include/eunit.hrl").
put_new_key_test() ->
Map = #{a => 1},
NewMap = hb_maps:put(b, 2, Map),
?assertEqual(#{a => 1, b => 2}, NewMap).
put_update_existing_test() ->
Map = #{a => 1},
NewMap = hb_maps:put(a, 2, Map),
?assertEqual(#{a => 2}, NewMap).4. map/2, map/3
-spec map(Fun, Map, Opts) -> Map
when
Fun :: fun((Key, Value) -> NewValue),
Map :: map(),
Opts :: map().Description: Transform all values in map using function. Both map and each value are loaded before processing.
Test Code:-module(hb_maps_map_test).
-include_lib("eunit/include/eunit.hrl").
map_with_link_test() ->
Bin = <<"TEST DATA">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{}}, 3 => 3},
Result = hb_maps:map(fun(_K, V) -> V end, Map, #{}),
?assertEqual(#{1 => 1, 2 => Bin, 3 => 3}, Result).
map_transform_test() ->
Map = #{a => 1, b => 2, c => 3},
Result = hb_maps:map(fun(_K, V) -> V * 2 end, Map),
?assertEqual(#{a => 2, b => 4, c => 6}, Result).5. filter/2, filter/3
-spec filter(Fun, Map, Opts) -> Map
when
Fun :: fun((Key, Value) -> boolean()),
Map :: map(),
Opts :: map().Description: Keep only key-value pairs where function returns true. Values are loaded before testing. Important: Loaded values are kept even if filtered out (passive loading).
-module(hb_maps_filter_test).
-include_lib("eunit/include/eunit.hrl").
filter_with_link_test() ->
Bin = <<"TEST DATA">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{}}, 3 => 3},
Result = hb_maps:filter(fun(_, V) -> V =/= Bin end, Map),
?assertEqual(#{1 => 1, 3 => 3}, Result).
filter_passively_loads_test() ->
Bin = <<"TEST DATA">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{}}, 3 => 3},
% Filter that keeps everything
Result = hb_maps:filter(fun(_, _) -> true end, Map),
% Link is resolved to actual value
?assertEqual(#{1 => 1, 2 => <<"TEST DATA">>, 3 => 3}, Result).6. filtermap/2, filtermap/3
-spec filtermap(Fun, Map, Opts) -> Map
when
Fun :: fun((Key, Value) -> {true, NewValue} | false),
Map :: map(),
Opts :: map().Description: Filter and transform map simultaneously. Function returns {true, NewValue} to keep with transformation, or false to discard.
-module(hb_maps_filtermap_test).
-include_lib("eunit/include/eunit.hrl").
filtermap_with_link_test() ->
Bin = <<"TEST DATA">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{}}, 3 => 3},
Result = hb_maps:filtermap(
fun(_, <<"TEST DATA">>) -> {true, <<"FOUND">>};
(_K, _V) -> false
end,
Map
),
?assertEqual(#{2 => <<"FOUND">>}, Result).
filtermap_passively_loads_test() ->
Bin = <<"TEST DATA">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{}}, 3 => 3},
Result = hb_maps:filtermap(fun(_, V) -> {true, V} end, Map),
?assertEqual(#{1 => 1, 2 => <<"TEST DATA">>, 3 => 3}, Result).7. fold/3, fold/4
-spec fold(Fun, Acc, Map, Opts) -> Acc
when
Fun :: fun((Key, Value, Acc) -> NewAcc),
Acc :: term(),
Map :: map(),
Opts :: map().Description: Reduce map to single value by iterating over all key-value pairs. Values are loaded before processing.
Test Code:-module(hb_maps_fold_test).
-include_lib("eunit/include/eunit.hrl").
fold_with_typed_link_test() ->
Bin = <<"123">>,
{ok, Location} = hb_cache:write(Bin, #{}),
Map = #{1 => 1, 2 => {link, Location, #{<<"type">> => integer}}, 3 => 3},
Sum = hb_maps:fold(fun(_, V, Acc) -> V + Acc end, 0, Map),
?assertEqual(127, Sum).
fold_count_test() ->
Map = #{a => 1, b => 2, c => 3},
Count = hb_maps:fold(fun(_, _, Acc) -> Acc + 1 end, 0, Map),
?assertEqual(3, Count).8. merge/2, merge/3
-spec merge(Map1, Map2, Opts) -> Map
when
Map1 :: map(),
Map2 :: map(),
Opts :: map().Description: Merge two maps, with Map2 taking precedence. Both maps are loaded before merging.
Test Code:-module(hb_maps_merge_test).
-include_lib("eunit/include/eunit.hrl").
merge_basic_test() ->
Map1 = #{a => 1, b => 2},
Map2 = #{b => 3, c => 4},
Result = hb_maps:merge(Map1, Map2),
?assertEqual(#{a => 1, b => 3, c => 4}, Result).9. with/2, with/3, without/2, without/3
-spec with(Keys, Map, Opts) -> Map
when
Keys :: [term()],
Map :: map(),
Opts :: map().
-spec without(Keys, Map, Opts) -> Map
when
Keys :: [term()],
Map :: map(),
Opts :: map().Description: Keep only specified keys (with) or remove specified keys (without). Map is loaded before filtering.
-module(hb_maps_with_without_test).
-include_lib("eunit/include/eunit.hrl").
with_test() ->
Map = #{a => 1, b => 2, c => 3},
Result = hb_maps:with([a, c], Map),
?assertEqual(#{a => 1, c => 3}, Result).
without_test() ->
Map = #{a => 1, b => 2, c => 3},
Result = hb_maps:without([b], Map),
?assertEqual(#{a => 1, c => 3}, Result).Link Resolution Behavior
Automatic Loading
All functions automatically load:- The map itself (if passed as link)
- Each value accessed/processed (if it's a link)
% Map stored in cache
{ok, MapID} = hb_cache:write(#{key => value}, #{}),
MapLink = {link, MapID, #{}},
% Value stored in cache
{ok, ValueID} = hb_cache:write(<<"data">>, #{}),
ValueLink = {link, ValueID, #{}},
% Both are loaded automatically
ActualMap = #{key => ValueLink},
hb_cache:write(ActualMap, #{}),
% This resolves both links
Value = hb_maps:get(key, MapLink).
% Result: <<"data">>Typed Links
Links can specify type information for automatic conversion:
% Binary "123" stored as integer type
{ok, ID} = hb_cache:write(<<"123">>, #{}),
Link = {link, ID, #{<<"type">> => integer}},
Value = hb_maps:get(key, #{key => Link}).
% Result: 123 (integer, not binary)Passive Loading
Filter operations load ALL values, even those not kept:
Map = #{
a => {link, ID1, #{}},
b => {link, ID2, #{}},
c => {link, ID3, #{}}
},
% Loads ALL three values, keeps two
Result = hb_maps:filter(fun(K, _) -> K =/= b end, Map).
% All values loaded from cache, but only a and c kept in resultCommon Patterns
%% Basic map operations with link resolution
Map = #{key => {link, ID, #{}}},
Value = hb_maps:get(key, Map), % Loads from cache
%% Transform values (all loaded)
Transformed = hb_maps:map(
fun(_K, V) -> process(V) end,
Map
),
%% Filter with loaded values
Filtered = hb_maps:filter(
fun(_K, V) -> is_binary(V) end,
Map
),
%% Accumulate over map
Sum = hb_maps:fold(
fun(_K, V, Acc) -> V + Acc end,
0,
Numbers
),
%% Merge maps with link resolution
Combined = hb_maps:merge(Map1, Map2),
%% Project map to specific keys
Subset = hb_maps:with([key1, key2], Map),
%% Remove specific keys
Cleaned = hb_maps:without([temp, debug], Map),
%% Convert with loaded values
List = hb_maps:to_list(Map),
%% Check key existence (map loaded)
Exists = hb_maps:is_key(key, Map),
%% Get all keys/values (map loaded, values loaded)
Keys = hb_maps:keys(Map),
Values = hb_maps:values(Map),
%% Map size (map loaded)
Size = hb_maps:size(Map).Performance Considerations
- Cache Hits: Each link resolution requires cache lookup
- Recursive Loading: Nested links load recursively
- Passive Loading: Filter operations load ALL values
- No Lazy Evaluation: All accessed values loaded immediately
- Memory: Loaded values kept in memory
% Bad: Loads all values just to count
Size = hb_maps:size(Map).
% Good: Use maps:size directly if no links
Size = maps:size(Map).
% Bad: Filter loads everything
Small = hb_maps:filter(fun(K, _) -> K < 10 end, HugeMap).
% Good: Use maps:filter if values aren't links
Small = maps:filter(fun(K, _) -> K < 10 end, HugeMap).When to Use vs hb_ao
Usehb_maps when:
- ✅ Working with TABM format directly
- ✅ Performance critical (skip device processing)
- ✅ Message known to be
~message@1.0 - ✅ Only need link resolution, not full protocol
hb_ao when:
- ✅ Working with structured messages
- ✅ Need device-specific behavior
- ✅ Require key normalization
- ✅ Want type conversions
- ✅ Most of the time (default choice)
Integration with hb_ao
% hb_ao internally uses hb_maps for link resolution
% but adds full protocol support:
% Low-level (hb_maps)
Value = hb_maps:get(<<"key">>, TABMMsg),
% High-level (hb_ao) - preferred
{ok, Value} = hb_ao:get(<<"key">>, StructuredMsg, Opts).
% hb_ao adds:
% - Key normalization
% - Device function execution
% - Type conversions
% - Error handlingError Handling
% Missing key with default
Value = hb_maps:get(missing, Map, default).
% Result: default
% Missing key without default
Value = hb_maps:get(missing, Map).
% Result: undefined
% find returns error
case hb_maps:find(key, Map) of
{ok, Value} -> process(Value);
error -> handle_missing()
end.
% Link not in cache
% Throws exception from hb_cache:ensure_loaded
try
Value = hb_maps:get(key, Map)
catch
throw:{could_not_load_link, _} ->
{error, broken_link}
end.References
- Cache System -
hb_cache.erl - AO Core -
hb_ao.erl(prefer this for most use cases) - Links -
hb_link.erl - Standard Maps -
mapsmodule documentation
Notes
- Warning Repeated: Rarely use this module directly - prefer
hb_ao - Link Resolution: Automatic for both maps and values
- Passive Loading: Filter operations load all values
- No Lazy Eval: All accessed values loaded immediately
- Type Support: Typed links convert automatically
- Performance: Faster than
hb_aobut less safe - TABM Only: Assumes messages in TABM format
- No Device Logic: Skips device-specific processing
- Cache Dependent: Requires all links in cache
- Recursive: Handles nested link structures
- Drop-in Replacement: Same API as
mapsmodule - Opts Parameter: Most functions accept optional
Opts - Default Values: Supported like standard
maps - Error Propagation: Cache errors propagate up
- Use Sparingly: Only when performance critical and safe