Question about receive bloc of processes

I have a question about receive block syntax.
In the following code what is the arrow construct called ?

defmodule App do
	
	def loop do
		data = receive do
			item->item
		end	
            IO.inspect data
		loop
	end
end

The way I “look” at this is that the variable to the left of the arrow (item) represents the data that is sent to the process and the data to the right of the arrow returns some new data that references the variable to the left of the arrow (hence why the variable of the same name is used on the right) . The result of this is stored in the data variable. The entire thing “feels” similar to invoking a function that was written using a callback that has a predefined argument.

It is useful for me to look at the arrow as a callback function but nowhere else in elixir do I see a single arrow being used in the same way as it is in the receive block. So technically, what is this arrows behavior described as and what is it called ?

It’s pattern matching, similar to case etc. here https://elixir-lang.org/getting-started/case-cond-and-if.html

To expand a little… When you send the process waiting in receive a message, it tries to match it against the cases in the receive block. As you’ve only one case and it’s a variable then the message gets bound to that variable and the body of the case is executed. The body is just the variable that’s been bound and as everything is an expression it gets returned.

If you did something like:

def loop do
  data = receive do
     :specific_message -> "blah"
     item -> item
  end
end

Then if you sent :specific_message it would match that and return “blah” and anything that didn’t match :specific_message would be tried with the next match, which catches everything and binds it to item.

1 Like

Thank you.

I don’t understand what you mean when you say:

“Then if you sent :specific_message it would match that and return “blah””

If I run the following code I don’t see “blah” returned from anything.

defmodule App do

	def loop do
	  data = receive do
	     :specific_message -> "blah"
	 
	  end
	  IO.inspect data
	  data
	end
  
end


pid = spawn(App, :loop, [])
IO.inspect send(pid,"weeeeeee")

In your code only :specific_message will match. Try send(pid, :specific_message) and you’ll see "blah" output.

Make your receive block:

data = receive do
  :specific_message -> "blah"
  other_message -> other_message
end

And then try send(pid, "weeeeeeeee") and send(pid, :specific_message) to see the pattern matching in action.

1 Like
def loop do
  data =
    receive do
      # This will only match on the atom `:specific_message`
      :specific_message ->
        # `data` will be bound to the string `"blah"`
        "blah"

      # Since this is a variable match it will match on anything the spawned process receives
      other_data ->
        # `data` will be bound to the string `"Other data: <whatever you sent to the process>"`
        "Other data: #{other_data}"
    end

  # This does not return anything, it only prints it inside the receiving process
  IO.inspect(data)

  # We call the loop function here in the running process in order to be able to receive more
  # messages even though we've already matched on an incoming message
  loop()
end
iex(1)> looper = spawn(Sandbox, :loop, [])
#PID<0.125.0>
iex(2)> send(looper, :specific_message) # note that we send the atom `:specific_message`
"blah" # "bla" is printed because it matched in the receive loop
:specific_message # The return value of `send` is always what we sent
iex(3)> send(looper, "other message here")
"Other data: other message here" # this is printed because we matched on the open clause in the `receive`
"other message here" # return value from `send`

Be sure to not confuse the return value of send with some kind of return value from the process you’re sending to. Processes can’t return anything to you. They can only send messages back to you. This is managed automatically for you in OTP.

2 Likes

The construct is identical to the one in the case expression (special form).

Matches the given expression against the given clauses.

The Erlang documentation breaks it down to the constituent parts:

Pattern [when GuardSeq] -> Body

  • Pattern to be matched
  • GuardSeq - the guard sequence to be satisfied (when GuardSeq is entirely optional)
  • Body - the sequence of expressions to be evaluated if and only if both Pattern is matched and GuardSeq is satisfied. The result of the last evaluated expression becomes the value of the receive (or case).

To demonstrate that the clauses work the same way inside a receive and a case:

show_next_msg = fn ->
  receive do
    msg -> IO.puts("Next Msg: #{inspect msg}") # MATCHES ANY message
  after
    500 ->
      IO.puts("Next Msg: timed out")
  end
end

spawn_process = fn fun ->
  pid = Process.spawn(fun, [])
  IO.puts("Spawn Process: #{inspect pid}")
  pid
end

alive? = fn pid ->
  IO.puts("#{inspect pid} is alive: #{Process.alive?(pid)}")
end

ping = fn to ->
  Kernel.send(to, {:ping, self()})
end

pong = fn to ->
  Kernel.send(to, {:pong, self(), to})
end

receive_fn = fn ->
  receive do
    {:ping, from} ->
      pong.(from) # Process blocks until MATCHING message
  end
end

receive_case_fn = fn ->
  receive do
    msg ->               # MATCHES anything
      case msg do
        {:ping, from} -> # identical to the one in receive_fn
          pong.(from)
        other ->         # MATCHES anything else
          IO.puts("WTH: #{inspect other}")
      end
  end
end

# ---

primary_pid = self() # PID of the primary PID
IO.puts "Primary PID: #{inspect primary_pid}"

IO.puts("receive_fn")
other_pid1 = spawn_process.(receive_fn)
alive?.(other_pid1)
ping.(other_pid1)                # Send :ping to other_pid1
show_next_msg.()
alive?.(other_pid1)              # i.e. other_pid1 terminated after receive

other_pid2 = spawn_process.(receive_fn)
alive?.(other_pid2)
Kernel.send(other_pid2, :junk)  # i.e. other_pid2 doesn't have a match for :junk
show_next_msg.()
alive?.(other_pid2)
ping.(other_pid2)               # other_pid2 will MATCH this - bypasing :junk
show_next_msg.()
alive?.(other_pid2)

IO.puts("receive_case_fn")
other_pid3 = spawn_process.(receive_case_fn)
alive?.(other_pid3)
ping.(other_pid3)                # Send :ping to other_pid3
show_next_msg.()
alive?.(other_pid3)              # i.e. other_pid3 terminated after receive

other_pid4 = spawn_process.(receive_case_fn)
alive?.(other_pid4)
Kernel.send(other_pid4, :junk)  # i.e. other_pid4 receive takes ANYTHING
show_next_msg.()
alive?.(other_pid4)
$ elixir demo.exs
Primary PID: #PID<0.73.0>
receive_fn
Spawn Process: #PID<0.76.0>
#PID<0.76.0> is alive: true
Next Msg: {:pong, #PID<0.76.0>, #PID<0.73.0>}
#PID<0.76.0> is alive: false
Spawn Process: #PID<0.77.0>
#PID<0.77.0> is alive: true
Next Msg: timed out
#PID<0.77.0> is alive: true
Next Msg: {:pong, #PID<0.77.0>, #PID<0.73.0>}
#PID<0.77.0> is alive: false
receive_case_fn
Spawn Process: #PID<0.78.0>
#PID<0.78.0> is alive: true
Next Msg: {:pong, #PID<0.78.0>, #PID<0.73.0>}
#PID<0.78.0> is alive: false
Spawn Process: #PID<0.79.0>
#PID<0.79.0> is alive: true
WTH: :junk
Next Msg: timed out
#PID<0.79.0> is alive: false
$

The important difference is that receive_fn leaves :junk in the process mailbox because its receive does not have a clause to match :junk. The receive in receive_case_fn will remove ANY message from the process mailbox because of the match-ALL msg pattern - that is how :junk ends up in the other (catch-ALL) clause of the case expression.

Thanks, I see it now.

In case anyone in the future reads this thread this little example makes it even clearer IMHO.

defmodule App do
  def loop do
    data =
      receive do
        :hard_coded_data_option_1 -> "thing-1"
        :hard_coded_data_option_2 -> "thing-2"
        dynamicallyCreatedData -> dynamicallyCreatedData
      end

    IO.inspect(data)
    loop

  end
end

pid = spawn(App, :loop, [])
send(pid, :hard_coded_data_option_1) #"thing-1"
send(pid, :hard_coded_data_option_2) #"thing-2"
send(pid, "Hi Mom!") #Hi Mom!

Just to be 100% clear because I saw you previously had a part in this post where you said receives are “bi-directional”:

This loop will never ever return anything. The return value you’re seeing when you’re using send is coming only from the send function call, because it always returns whatever you sent.

If a process actually wants a response to something it will have to make sure that the process that should send the response knows where to reach it (a pid) and then it’ll send a message with the response. The process that should receive that response must also know to actually be in a receive loop.

I said pattern matching

a = 100

100 = a

And I deleted it.

You’re right about the return comment, I should have said “logs”, but then again - I deleted it :slight_smile: