%% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at https://mozilla.org/MPL/2.0/. %% %% Copyright (c) 2017-2026 Broadcom. All Rights Reserved. The term Broadcom %% refers to Broadcom Inc. and/or its subsidiaries. -module(ra_lol). %% Adaptive sorted list structure that uses: %% - Simple list for small collections (< 65 elements) - fast pattern matching %% - Tuple with binary search for larger collections (>= 65 elements) %% %% This provides optimal performance across all sizes. -export([ new/0, new/1, append/2, search/2, takewhile/2, foldl/3, foldr/3, from_list/1, from_list/2, to_list/1, len/1 ]). %% Threshold at or above which we use tuple, below we use simple list -define(TUPLE_THRESHOLD, 65). -type gt_fun() :: fun((Item, Item) -> boolean()). %% State is either: %% - {list, GtFun, List} where List is stored in descending order (newest first) %% - {tuple, GtFun, Len, Data} where Data is a tuple in descending order -opaque state() :: {list, gt_fun(), list()} | {tuple, gt_fun(), non_neg_integer(), tuple()}. %% Continuation is either: %% - A simple list (remaining elements to search) %% - Tuple continuation {Data, Pos, High} -opaque cont() :: list() | {tuple(), pos_integer(), non_neg_integer()}. -export_type([state/0, cont/0]). -spec new() -> state(). new() -> {list, fun erlang:'>'/2, []}. -spec new(gt_fun()) -> state(). new(GtFun) -> {list, GtFun, []}. %% @doc append an item that is greater than the last appended item -spec append(Item, state()) -> state() | out_of_order when Item :: term(). append(Item, {list, GtFun, []}) -> {list, GtFun, [Item]}; append(Item, {list, GtFun, [Last | _] = List}) -> case GtFun(Item, Last) of true -> NewList = [Item | List], maybe_upgrade(GtFun, NewList); false -> out_of_order end; append(Item, {tuple, GtFun, Len, Data}) -> %% Last appended item is at index 1 (newest first) LastItem = element(1, Data), case GtFun(Item, LastItem) of true -> %% Prepend by converting to list and back - O(N) NewData = list_to_tuple([Item | tuple_to_list(Data)]), {tuple, GtFun, Len + 1, NewData}; false -> out_of_order end. -spec search(fun((term()) -> higher | lower | equal), state() | cont()) -> {term(), cont()} | undefined. search(_SearchFun, {list, _GtFun, []}) -> undefined; search(SearchFun, {list, _GtFun, List}) -> list_search(SearchFun, List); search(_SearchFun, {tuple, _GtFun, 0, _Data}) -> undefined; search(SearchFun, {tuple, _GtFun, Len, Data}) -> %% Use binary search for tuple binary_search(SearchFun, Data, 1, Len); search(SearchFun, Cont) when is_list(Cont) -> %% List continuation - continue searching the remaining list list_search(SearchFun, Cont); search(SearchFun, {Data, Pos, High}) when is_tuple(Data), Pos =< High -> %% Tuple continuation - use linear scan for sequential access tuple_linear_search(SearchFun, Data, Pos, High); search(_SearchFun, {Data, _Pos, _High}) when is_tuple(Data) -> undefined. %% Simple list search with fast pattern matching list_search(_SearchFun, []) -> undefined; list_search(SearchFun, [Item | Rest]) -> case SearchFun(Item) of equal -> %% Found! Return item and remaining list as continuation {Item, Rest}; lower -> %% Keep searching (toward smaller/older items) list_search(SearchFun, Rest); higher -> %% We've gone past where the item would be undefined end. %% Binary search for tuple - O(log N) %% Data is sorted descending (index 1 = largest/newest) binary_search(_SearchFun, _Data, Low, High) when Low > High -> undefined; binary_search(SearchFun, Data, Low, High) -> Mid = (Low + High) div 2, Item = element(Mid, Data), case SearchFun(Item) of equal -> %% Found! Continuation points to next element {Item, {Data, Mid + 1, tuple_size(Data)}}; higher -> %% Target is "higher" - in descending order, at lower indices binary_search(SearchFun, Data, Low, Mid - 1); lower -> %% Target is "lower" - in descending order, at higher indices binary_search(SearchFun, Data, Mid + 1, High) end. %% Linear search on tuple for continuations - O(1) amortized for sequential access tuple_linear_search(SearchFun, Data, Pos, High) when Pos =< High -> Item = element(Pos, Data), case SearchFun(Item) of equal -> {Item, {Data, Pos + 1, High}}; lower -> tuple_linear_search(SearchFun, Data, Pos + 1, High); higher -> undefined end; tuple_linear_search(_SearchFun, _Data, _Pos, _High) -> undefined. -spec takewhile(fun((Item) -> boolean()), state()) -> {[Item], state()} when Item :: term(). takewhile(Fun, {list, GtFun, List}) -> {Taken, Left} = lists:splitwith(Fun, List), {Taken, {list, GtFun, Left}}; takewhile(Fun, {tuple, GtFun, _Len, Data}) -> List = tuple_to_list(Data), {Taken, Left} = lists:splitwith(Fun, List), %% Rebuild appropriate structure based on remaining size LeftLen = length(Left), NewState = if LeftLen >= ?TUPLE_THRESHOLD -> {tuple, GtFun, LeftLen, list_to_tuple(Left)}; true -> {list, GtFun, Left} end, {Taken, NewState}. %% @doc initialise from a list sorted in descending order -spec from_list(list()) -> state(). from_list(List) -> from_list(fun erlang:'>'/2, List). -spec from_list(gt_fun(), list()) -> state(). from_list(GtFun, List) when is_list(List) -> Len = length(List), if Len >= ?TUPLE_THRESHOLD -> %% Store in descending order (newest/largest first) as tuple {tuple, GtFun, Len, list_to_tuple(List)}; true -> %% Store in descending order as list {list, GtFun, List} end. -spec to_list(state()) -> list(). to_list({list, _GtFun, List}) -> List; to_list({tuple, _GtFun, _Len, Data}) -> tuple_to_list(Data). -spec len(state()) -> non_neg_integer(). len({list, _GtFun, List}) -> length(List); len({tuple, _GtFun, Len, _Data}) -> Len. %% @doc Fold left-to-right (from newest/largest to oldest/smallest). %% Since the structure stores items in descending order (newest first), %% this iterates from the beginning to the end. -spec foldl(fun((Item, Acc) -> Acc), Acc, state()) -> Acc when Item :: term(), Acc :: term(). foldl(Fun, Acc, {list, _GtFun, List}) -> lists:foldl(Fun, Acc, List); foldl(Fun, Acc, {tuple, _GtFun, Len, Data}) -> tuple_foldl(Fun, Acc, Data, 1, Len). tuple_foldl(_Fun, Acc, _Data, Pos, Len) when Pos > Len -> Acc; tuple_foldl(Fun, Acc, Data, Pos, Len) -> tuple_foldl(Fun, Fun(element(Pos, Data), Acc), Data, Pos + 1, Len). %% @doc Fold right-to-left (from oldest/smallest to newest/largest). %% Since the structure stores items in descending order (newest first), %% this iterates from the end to the beginning. -spec foldr(fun((Item, Acc) -> Acc), Acc, state()) -> Acc when Item :: term(), Acc :: term(). foldr(Fun, Acc, {list, _GtFun, List}) -> lists:foldr(Fun, Acc, List); foldr(Fun, Acc, {tuple, _GtFun, Len, Data}) -> tuple_foldr(Fun, Acc, Data, Len). tuple_foldr(_Fun, Acc, _Data, 0) -> Acc; tuple_foldr(Fun, Acc, Data, Pos) -> tuple_foldr(Fun, Fun(element(Pos, Data), Acc), Data, Pos - 1). %%% =================== %%% Internal functions %%% =================== %% Upgrade from list to tuple if we've crossed the threshold maybe_upgrade(GtFun, List) -> Len = length(List), if Len >= ?TUPLE_THRESHOLD -> %% Convert to tuple {tuple, GtFun, Len, list_to_tuple(List)}; true -> {list, GtFun, List} end. %%% =================== %%% Internal unit tests %%% =================== -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). basic_test() -> Items = lists:seq(1, 100), L0 = ?MODULE:from_list(lists:reverse(Items)), ?assertEqual(100, ?MODULE:len(L0)), ?assertEqual(Items, lists:reverse(?MODULE:to_list(L0))), ?assertMatch(out_of_order, ?MODULE:append(1, L0)), L1 = ?MODULE:append(101, L0), ?assertEqual(101, ?MODULE:len(L1)), SearchFun = fun (T) -> fun (Item) -> if T == Item -> equal; T > Item -> higher; true -> lower end end end, [begin {T, _} = ?MODULE:search(SearchFun(T), L1), ok end || T <- Items ++ [101]], %% test searching with a continuation _ = lists:foldl(fun (T, Acc) -> {T, Cont} = ?MODULE:search(SearchFun(T), Acc), Cont end, L1, lists:reverse(Items ++ [101])), TakeFun = fun(Item) -> Item > 50 end, {Taken, L2} = takewhile(TakeFun, L1), ?assertEqual(50, ?MODULE:len(L2)), ?assertEqual(51, length(Taken)), ?assertMatch(out_of_order, ?MODULE:append(50, L2)), L3 = ?MODULE:append(51, L2), ?assertEqual(51, ?MODULE:len(L3)), ok. %% Test that small lists use simple list small_uses_list_test() -> Items = lists:seq(1, 20), {list, _, _} = ?MODULE:from_list(lists:reverse(Items)). %% Test that large lists use tuple large_uses_tuple_test() -> Items = lists:seq(1, 100), {tuple, _, _, _} = ?MODULE:from_list(lists:reverse(Items)). %% Test upgrade from list to tuple via append upgrade_test() -> Items = lists:seq(1, 64), L0 = ?MODULE:from_list(lists:reverse(Items)), {list, _, _} = L0, L1 = ?MODULE:append(65, L0), {tuple, _, _, _} = L1, ?assertEqual(65, ?MODULE:len(L1)). %% Test foldl - iterates from newest to oldest (high to low) foldl_test() -> %% Small list (uses list representation) SmallItems = lists:seq(1, 20), SmallLol = ?MODULE:from_list(lists:reverse(SmallItems)), %% foldl iterates from newest (20) to oldest (1) %% Prepending gives us [1, 2, ..., 20] (oldest to newest) SmallFoldlResult = ?MODULE:foldl(fun(Item, Acc) -> [Item | Acc] end, [], SmallLol), ?assertEqual(SmallItems, SmallFoldlResult), %% Large list (uses tuple representation) LargeItems = lists:seq(1, 100), LargeLol = ?MODULE:from_list(lists:reverse(LargeItems)), LargeFoldlResult = ?MODULE:foldl(fun(Item, Acc) -> [Item | Acc] end, [], LargeLol), ?assertEqual(LargeItems, LargeFoldlResult), %% Test with sum accumulator SumResult = ?MODULE:foldl(fun(Item, Acc) -> Item + Acc end, 0, LargeLol), ?assertEqual(lists:sum(LargeItems), SumResult), %% Empty list EmptyLol = ?MODULE:new(), EmptyResult = ?MODULE:foldl(fun(Item, Acc) -> [Item | Acc] end, [], EmptyLol), ?assertEqual([], EmptyResult). %% Test foldr - iterates from oldest to newest (low to high) foldr_test() -> %% Small list (uses list representation) SmallItems = lists:seq(1, 20), SmallLol = ?MODULE:from_list(lists:reverse(SmallItems)), %% foldr iterates from oldest (1) to newest (20) %% Prepending gives us [20, 19, ..., 1] (newest to oldest, same as to_list) SmallFoldrResult = ?MODULE:foldr(fun(Item, Acc) -> [Item | Acc] end, [], SmallLol), ?assertEqual(?MODULE:to_list(SmallLol), SmallFoldrResult), %% Large list (uses tuple representation) LargeItems = lists:seq(1, 100), LargeLol = ?MODULE:from_list(lists:reverse(LargeItems)), LargeFoldrResult = ?MODULE:foldr(fun(Item, Acc) -> [Item | Acc] end, [], LargeLol), ?assertEqual(?MODULE:to_list(LargeLol), LargeFoldrResult), %% Test with sum accumulator SumResult = ?MODULE:foldr(fun(Item, Acc) -> Item + Acc end, 0, LargeLol), ?assertEqual(lists:sum(LargeItems), SumResult), %% Empty list EmptyLol = ?MODULE:new(), EmptyResult = ?MODULE:foldr(fun(Item, Acc) -> [Item | Acc] end, [], EmptyLol), ?assertEqual([], EmptyResult). -endif.