I’d like to write a function that receives all messages in the inbox and returns them as a list. It should be a dead-simple task, but I can’t quite trace the recursion.
def receive_messages(timeout) do
receive do
data ->
case receive_messages(timeout) do
{:ok, next_data} ->
{:ok, [data | next_data]}
:empty ->
{:ok, data}
end
after
timeout ->
IO.puts("No messages in inbox.")
:empty
end
end
Expected Output (If There Are Messages)
{:ok, ["Message from #1.", "Message from #2.", "Message from #3."]}.
Actual Output (If There Are Messages)
{:ok, ["Message from #1.", "Message from #2." | "Message from #3."]}.
There’s something fishy about my receive_messages/1, but I just can’t fix it. I should probably split the function into several cases, too, but I couldn’t figure out how they should be structured.
Your :empty case should probably return a list as data :
def receive_messages(timeout) do
receive do
data ->
case receive_messages(timeout) do
{:ok, next_data} ->
{:ok, [data | next_data]}
:empty ->
{:ok, [data]} # here
end
after
timeout ->
IO.puts("No messages in inbox.")
:empty
end
end
Otherwise, your last next_data will not be a list, creating "Message from #2." | "Message from #3." by concatenation :
def receive_messages(timeout, acc \\ []) do
receive do
msg -> receive_messages(timeout, [msg | acc])
after
timeout ->
case acc do
[] -> :empty
_ -> {:ok, :lists.reverse(acc)}
end
end
end
Also note that if you can pattern match on the empty list outside of the function. Sometimes it does not make sense to have :empty as a special case.
So that could be:
def receive_messages(timeout, acc \\ []) do
receive do
msg -> receive_messages(timeout, [msg | acc])
after
timeout -> {:ok, :lists.reverse(acc)}
end
end
And it just returns {:ok, []} if no messages were received.
And now, since it cannot return {:error, _} or :empty I would just remove the tuple and return a list:
def receive_messages(timeout, acc \\ []) do
receive do
msg -> receive_messages(timeout, [msg | acc])
after
timeout -> :lists.reverse(acc)
end
end
That’s true. I removed it and the code’s cleaner for it. I don’t think I can pattern-match on an empty map, but I can simply use Enum.empty?/1. I’d probably have used an if statement regardless.
def dispatch() do
timeout = 10
messages = receive_messages(timeout)
if not Enum.empty?(messages) do
# ...
end
Process.sleep(1000)
dispatch()
end
Nope. I’ll try to pop a single item off the enumerable and if there is one returns false otherwise the enumerable is empty. It doesn’t enumerate everything.
Even if it’s constant time it’s still more work though:
map = for i <- 1..5000, into: %{}, do: {i, System.unique_integer()}
Benchee.run(%{
"map_size" => fn -> map_size(map) > 0 end,
"pattern" => fn -> map != %{} end
})
Operating System: Linux
CPU Information: AMD Ryzen 3 3200G with Radeon Vega Graphics
Number of Available Cores: 4
Available memory: 5.80 GB
Elixir 1.13.2
Erlang 24.1.7
Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s
Benchmarking map_size ...
Benchmarking pattern ...
Name ips average deviation median 99th %
pattern 1.45 M 0.69 μs ±5168.36% 0.47 μs 0.96 μs
map_size 0.93 M 1.07 μs ±2783.37% 0.77 μs 1.65 μs
Comparison:
pattern 1.45 M
map_size 0.93 M - 1.56x slower +0.38 μs