Current section
Files
Jump to
Current section
Files
lib/mutable_map.ex
defmodule MutableMap do
@moduledoc """
# MutableMap
Large, no worries mutable maps for Elixir. These `MutableMap`s can be used as drop-in replacements for `Map`s, but can also be used as a shared, mutable, concurrent data store.
Multiple processes can safely read and write to the same `MutableMap` without the need for locking or other synchronization mechanisms. When no process is using the `MutableMap` anymore, it is automatically garbage collected like normal Elixir Maps.
## Motivation
When working with large data sets, Elixir's `Map`s and `Keyword`s can become unwieldy. E.g. when using large maps (200mb+) then sending them between processes becomes really slow (Easily 50ms and more for one `send(pid, large_map)`). On the other hand Erlang `:ets` tables can solve this problem, but their API is unwieldy and they require explicit creation and cleaning up.
Instead `MutableMap`s offer an easy to use, performant and most importantly lazy garbage collected data storage solution.
## Usage
```elixir
# Create a new MutableMap
MutableMap.new()
# Create a new MutableMap from a list
MutableMap.new([{1, 2}, {3, 4}])
# Set and get values
MutableMap.put(map, 1, 2)
MutableMap.get(map, 1)
# Delete values
MutableMap.delete(map, 1)
# Since MutableMaps are mutable they can be explicitly copied to create different instances
map1 = MutableMap.new()
map2 = map1
MutableMap.put(map1, :a, 1)
MutableMap.to_list(map1)
# => [:a, 1]
MutableMap.to_list(map2)
# => [:a, 1]
map3 = MutableMap.new(map1)
MutableMap.put(map3, :b, 2)
MutableMap.to_list(map1)
# => [:a, 1]
MutableMap.to_list(map3) |> Enum.sort()
# => [:a, 1, :b, 2]
```
"""
defstruct [:ref, :table]
@type t :: %MutableMap{ref: reference(), table: reference()}
def new() do
{ref, ets} = MutableMap.Beacon.new_map_ref()
%MutableMap{ref: ref, table: ets}
end
def new(list) when is_list(list) do
import_list(new(), list)
end
def new(other) do
import_list(new(), Enum.to_list(other))
end
def get(map, key, default \\ nil) do
case :ets.lookup(map.table, key) do
[{_key, value}] -> value
[] -> default
end
end
def put(map, key, value) do
:ets.insert(map.table, {key, value})
map
end
def put_new(map, key, value) do
case :ets.lookup(map.table, key) do
[{_key, _value}] -> map
[] -> put(map, key, value)
end
end
def put_new_lazy(map, key, fun) do
case :ets.lookup(map.table, key) do
[{_key, _value}] -> map
[] -> put(map, key, fun.())
end
end
def delete(map, key) do
:ets.delete(map.table, key)
map
end
def update(map, key, default, fun) do
case :ets.lookup(map.table, key) do
[{_key, value}] -> put(map, key, fun.(value))
[] -> put(map, key, default)
end
map
end
def keys(map) do
:ets.select(map.table, [{{:"$1", :_}, [], [:"$1"]}])
end
def to_list(map) do
:ets.select(map.table, [{{:"$1", :"$2"}, [], [{{:"$1", :"$2"}}]}])
end
def import_list(map, list) do
:ets.insert(map.table, list)
map
end
def size(map) do
:ets.select_count(map.table, [{{:_, :_}, [], [true]}])
end
def empty?(map) do
size(map) == 0
end
def has_key?(map, key) do
:ets.lookup(map.table, key) != []
end
def reduce(map, acc, fun) do
:ets.foldl(fun, acc, map.table)
end
end
defimpl Enumerable, for: MutableMap do
def count(map) do
{:ok, MutableMap.size(map)}
end
def member?(map, key) do
{:ok, MutableMap.get(map, key) != []}
end
def reduce(map, acc, fun) do
Enumerable.reduce(MutableMap.to_list(map), acc, fun)
end
def slice(_map) do
{:error, __MODULE__}
end
end