utahplt / chorex

Choreographic programming in Elixir
https://hex.pm/packages/chorex
MIT License
14 stars 0 forks source link

Help with tiny counter choreography, module is not a behavior #6

Closed bennn closed 3 weeks ago

bennn commented 3 weeks ago

Figure 2 of this paper has a Counter example with 2 parties https://doi.org/10.1016/j.jlamp.2023.100891

Screenshot 2024-06-06 at 4 29 40 PM

I'm trying to turn it into a choreography.

First attempt:

defchor [Server, Client] do
      Client.incr() ~> Server.(value)
      Client.incr() ~> Server.(value)
      Client.incr() ~> Server.(value)
      Server.(value)
end

Moving on, I tried just deleting the first two lines. Full code:

defmodule Counter do
  defmodule Chor do
    import Chorex

    defchor [Server, Client] do
      # Client.incr() ~> Server.(value)
      # Client.incr() ~> Server.(value)
      Client.incr() ~> Server.(value)
      Server.(value)
    end
  end

  defmodule MyClient do
    use Chor.Chorex, :client
    def incr(), do: 1
  end

  defmodule MyServer do
    use Chor.Chorex, :server
  end
end
defmodule Counter do
  defmodule Chor do
    import Chorex

    defchor [Server, Client] do
      # Client.incr() ~> Server.(value)
      # Client.incr() ~> Server.(value)
      Client.incr() ~> Server.(value)
      Server.(value)
    end
  end

  defmodule MyClient do
    use Chor.Chorex, :client
    def incr(), do: 1
  end

  def kickoff() do
    pc = spawn(MyClient, :init, [])
    ps = spawn(MyServer, :init, [])

    config = %{Server => ps, Client => pc, :super => self()}

    send(pc, {:config, config})
    send(ps, {:config, config})

    # receive {:choreography_return, Server, 0}
    receive do
      {:choreography_return, Client, val} -> IO.puts("Got #{val}")
    end
  end

end

# make file kickoff.exs = Chor.Counter.kickoff()
# mix run kickoff.exs
bennn commented 3 weeks ago

Although mix run _ finishes without error, iex prints an error. iex also runs to completion:

>% iex -S mix
Erlang/OTP 26 [erts-14.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]

Interactive Elixir (1.16.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Counter.kickoff()
Got 1
:ok

16:35:21.168 [error] Process #PID<0.142.0> raised an exception
** (UndefinedFunctionError) function MyServer.init/0 is undefined (module MyServer is not available)
    MyServer.init()
ashton314 commented 3 weeks ago

This gives warnings that variable "value" is not used. Ok I guess the server needs some kind of functional update?

The choreography (roughly) expands to

  value = receive, do: m -> m    # this variable `value' gets overriden on the very next line
  value = receive, do: m -> m
  ...

which is why Elixir is complaining that value is never used. Elixir has no† mutation, so you'd want some kind of functional update.

†Every process has a mutable dictionary associated with it, but it's considered very bad form to use that.

ashton314 commented 3 weeks ago

I don't know what the output means: warning: module Server is not a behaviour

The module Server does not have any local functions that need implementing, so it's not considered a "behaviour". Chorex should be a little smarter about this: when you say use Chor.Chorex, :server it shouldn't try to expand to @behaviour Server if Server's interface is empty. This is what is happening now; we'll revisit once v0.2 is out.

ashton314 commented 3 weeks ago

Although mix run _ finishes without error, iex prints an error. iex also runs to completion

I think I see why this is happening.

Why we get back anything at all

In the case where both modules are implemented, both choreographies will finish with a return value of 1, as that is the last value they each compute. In the case of the Client actor specifically, its last line looks like:

send(config[Server], incr())

And Kernel.send/2 returns whatever the message was. So, Client returns with 1. This is why this code:

receive do
  {:choreography_return, Client, val} -> IO.puts("Got #{val}")
end

prints Got 1, because the Client sends the message (to a nonexistent process) and then returns that value, which gets picked up by the parent, printed out, and then the whole thing terminates.

On the error message

But why are we able to try to send a message in the first place? Why doesn't that immediately kill the Client process?

Well, that's because we have a valid PID that we're trying to send to. We got this PID when the parent said

ps = spawn(MyServer, :init, [])

MyServer didn't exist, so it threw a warning. However, it did still return a PID—nothing was running at that PID however. This is why we could pass it to send without blowing up, and why the Client process was able to complete.

Since iex is a long-running process, we got to see that error message. However, running mix run counter.exs exits too quickly for that error to get printed. Indeed, if you make this change:

receive do
  {:choreography_return, Client, val} -> IO.puts("Got #{val}")
end
Process.sleep(1000)  # Sleep for one second

then you do see the error message.

ashton314 commented 3 weeks ago

Please close if satisfied with answer.

bennn commented 3 weeks ago

Thanks!

What about the behavior question --- do you want to move that to a separate issue or leave this one open as the reminder?

ashton314 commented 3 weeks ago

I'll create a new issue.