9mm
How to quickly add to list in map?
Hello,
I have a collection like:
%{stats_a: [4, 5, 6], stats_b: [1, 2, 3]} # not actually integers (see next question)
Each of the 2 lists will be ~1 million items long.
I have a method which needs to push an item to beginning of each list. Ideally, it would in real-time chop the oldest/last items of the list so that it’s never longer than 1,000,000 items exactly
If theres no super fast way to do this however I will set a timer to do it every 1000ms… if there is an O(n) fast way to do it though I’ll do it in realtime. I need this timer anyway to aggregate stats so I’m not bothered if it needs to go there.
I will be pushing to it ~300-500 times/second.
Here’s what I had so far (without the limiting part yet…)
Is this crazy what I wrote?
def handle_cast({:push_stats, key, stats}, state) do
items = [stats | state[key]]
new_state = state |> Map.put(key, items)
{:noreply, new_state}
end
Most Liked
peerreynders
defmodule Demo do
def merge_values(_key, value1, value2),
do: value1 + value2
def reducer({key, data}, aggregate) do
merge_data = fn all_data -> Map.merge(all_data, data, &merge_values/3) end
Map.update(aggregate, key, data, merge_data)
end
def run(list),
do: List.foldl(list, %{}, &reducer/2)
end
list = [
{:group_1, %{timeout: 1, failure: 0, hits: 0}},
{:group_1, %{timeout: 0, failure: 0, hits: 0}},
{:group_2, %{timeout: 1, failure: 1, hits: 0}}
]
IO.inspect(Demo.run(list))
$ elixir demo.exs
%{
group_1: %{failure: 0, hits: 0, timeout: 1},
group_2: %{failure: 1, hits: 0, timeout: 1}
}
michalmuskala
One thing to consider might be using an ets table instead of a server with state. In general, if you need high performance storage, ets tables are a very good solution. You could still have a process periodically reading from the table and doing some statistics.
peerreynders
Obviously I don’t care about line count - I like code that is easy to change:
defmodule Demo do
defp initial_total(:response_time, time),
do: if(is_number(time), do: {time, 1}, else: {0, 0})
defp initial_total(_key, value),
do: value || 0
defp initial_aggregate({key, value}, aggregate),
do: Map.put(aggregate, key, initial_total(key, value))
defp merge_total(:response_time, {total_time, count} = total, time),
do: if(is_number(time), do: {total_time + time, count + 1}, else: total)
defp merge_total(_key, total, value),
do: total + (value || 0)
defp merge_aggregate({key, value}, aggregate) do
new_total =
case Map.fetch(aggregate, key) do
{:ok, total} ->
merge_total(key, total, value)
_ ->
initial_total(key, value)
end
Map.put(aggregate, key, new_total)
end
defp item_aggregator({key, data}, stats) do
new_aggregate =
case Map.fetch(stats, key) do
{:ok, aggregate} ->
Enum.reduce(data, aggregate, &merge_aggregate/2)
_ ->
Enum.reduce(data, %{}, &initial_aggregate/2)
end
Map.put(stats, key, new_aggregate)
end
defp finalize_total({:response_time, {total_time, count}}, aggregate) do
if count > 0 do
Map.put(aggregate, :response_time, div(total_time, count))
else
aggregate
end
end
defp finalize_total({key, total}, aggregate) do
Map.put(aggregate, key, total)
end
defp finalize_aggregate({key, aggregate}, stats),
do: Map.put(stats, key, Enum.reduce(aggregate, %{}, &finalize_total/2))
def make_stats(items) do
items
|> List.foldl(%{}, &item_aggregator/2)
|> Enum.reduce(%{}, &finalize_aggregate/2)
end
end
#
# item: {:group_1, data}
# data: %{timeout: 0, failure: 0, hits: 1, response_time: 100}
# value: associated with a "key" inside the "data" Map
# aggregate: %{failure: 0, hits: 2, response_time: {1100,2}, timeout: 0}
# total: value associated with a "key" inside the "aggregate" Map
# finalized_aggregate: %{failure: 0, hits: 2, response_time: 550, timeout: 0}
# stats: %{feed_key => (finalized_)aggregate}
#
feed_items = [
{:group_1, %{timeout: 0, failure: 0, hits: 1, response_time: 100}},
{:group_1, %{timeout: 0, failure: 0, hits: 1, response_time: 1000}},
{:group_2, %{timeout: 0, failure: 0, hits: 1, response_time: 50}},
{:group_2, %{timeout: 0, failure: 0, hits: 1, response_time: 2000}},
{:group_3, %{timeout: 0, failure: 1, hits: 0}},
{:group_3, %{timeout: 1, failure: 1, hits: 0, response_time: nil}}
]
IO.inspect(Demo.make_stats(feed_items))
$ elixir demo.exs
%{
group_1: %{failure: 0, hits: 2, response_time: 550, timeout: 0},
group_2: %{failure: 0, hits: 2, response_time: 1025, timeout: 0},
group_3: %{failure: 2, hits: 0, timeout: 1}
}
9mm
Damn that is intense, I need to reread this a few times… thank you. One thing about functional programming is this stuff gets a lot less intuitive/easy. hopefully it gets easeir in time








