How can I receive all messages in the inbox and return them as a list?

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.

Hi,

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 :

iex(1)> ["2" | "3"]        
["2" | "3"]
iex(2)> ["2" | ["3"]]
["2", "3"]
3 Likes

I would go with tail recursion on this one:

  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
2 Likes

Yes, that’s a lot simpler! I was having trouble following my own implementation.

I was planning to replace my use of a list—which was simpler to prototype—with a map, and I had an easier time with your code. Thank you.

1 Like

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
2 Likes

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

If your function returns a map then you can use if map_size(map) > 0

In my examples, the function returns a list, so you can do:

case receive_messages(100) do
  [] -> :ok
  msgs -> do_stuff(msgs)
end

Now if you call Enum.each/2 or something like that on your list of messages you do not care wether the list is empty or not.

1 Like

Is map_size/1 more performant than Enum.empty?/1? Because empty?/1 is more elegant.

When checking for empty states you don’t want to count the number of items. if map != %{} do will be more efficient.

1 Like

According to the docs, map_size/1 is done in constant time. I don’t know how Enum.empty?/1 works.

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
3 Likes