在 Elixir 进程之间传递状态值的问题 - 如何正确存储它?

Problem with passing state value between Elixir processes - how to store it correctly?

嗨,

我正在尝试编写 CLI - 日历。 问题是,当我通过 IEx 发送消息时,结果如预期的那样。但是,当我在其他函数的接收块中一条一条地发送这些消息时,输出显示好像一个列表(存储为进程中的状态[我猜..?])是空的。代码(示例消息将在后面):

第一个模块

defmodule ConcCalendar.Formatter do
  use Timex, TableRex

  def print_month(days, month, year) do
    header = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    months = %{1 => "January", 2 => "February", 3 => "March", 4 => "April", 5 => "May", 6 => "June", 7 => "July",
    8 => "August", 9 => "September", 10 => "October", 11 => "November", 12 => "December"}
    TableRex.quick_render!(days, header, months[month])
    |> IO.puts()
  end

  def year_manager(year, months) do

    receive do
      {:create_months, sender} ->
        months = for month <- 1..12 do spawn(ConcCalendar.Formatter, :month_manager, [month, year, [], []]) end
        for month_id <- months do
          send month_id, {:create_days}
        end
        year_manager(year, months)

      {:print_month, month} ->
        month_pid = Enum.at(months, month - 1)
        send month_pid, {:create_printable}
        send month_pid, {:set_printable}
        send month_pid, {:print}
        year_manager(year,months)
    end
  end


  def month_manager(month, year, days, printable) do

    receive do
      {:ok, day} ->
        printable = List.insert_at(printable, -1, day)
        month_manager(month, year, days, printable)

      {:create_days} ->
        days_in_month = 1..:calendar.last_day_of_the_month(year, month)
        days = Enum.map(days_in_month, &ConcCalendar.DayCreator.create_day_tuple/1)
        month_manager(month, year, days, printable)

      {:create_printable} ->
        for day_id <- days do
          send day_id, {:get_date, self()}
        end
        month_manager(month, year, days, printable)

      {:set_printable} ->
        Enum.sort(printable)
        first = :calendar.day_of_the_week(year, month, 1)
        offset = first - 1
        offset_list = []
        offset_list = for _x <- 1..offset do offset_list ++ [""] end
        printable = offset_list ++ printable
        printable = Enum.chunk_every(printable, 7)
        month_manager(month, year, days, printable)

      {:print} ->
        #send self(), {:create_printable}
        #send self(), {:set_printable}
        print_month(printable, month, year)
        month_manager(month, year, days, printable)
      end
    end
end

第二个模块

defmodule ConcCalendar.DayCreator do
  use Timex

  def create_day_tuple(day) do
    day_data = {day, "", ""}
    spawn(ConcCalendar.DayCreator, :day_tuple, [day_data])
  end

  def day_tuple(day_data) do
    {day, schedule, note} = day_data

    receive do
      {:schedule_a_meeting, sender} ->
        if schedule == "" do
          send sender, {:ok, "Successfully created a meeting"}
        else
          send sender, {:ok, "There is a meeting scheduled already"}
        end

        day_tuple({day, "*", note})

      {:cancel_a_meeting, sender} ->
        send sender, {:ok, "The meeting was canceled"}
        day_tuple({day, "", note})

      {:make_a_note, new_note, sender} ->
        send sender, {:ok, "Succesfully created a note"}
        day_tuple({day, schedule, new_note})

      {:delete_note, sender} ->
        send sender, {:ok, "The note was deleted"}
        day_tuple({day, schedule, ""})

      {:get_note, sender} ->
        send sender, {:ok, note}
        day_tuple(day_data)

      {:get_date, sender} ->
        send sender, {:ok, day}
        day_tuple(day_data)
    end
  end
end

用法

所以我的目标是创建一个 year_manager 流程,然后打印一个示例月份。 这是我得到的:

iex(1)> year1 = spawn(ConcCalendar.Formatter, :year_manager, [2021, []])
#PID<0.253.0>
iex(2)> send year1, {:create_months, self()}
{:create_months, #PID<0.251.0>}
iex(3)> send year1, {:print_month, 3}
{:print_month, 3}
+-----------------------------------------+
|                  March                  |
+-----+-----+-----+-----+-----+-----+-----+
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
+-----+-----+-----+-----+-----+-----+-----+
|     |     |
+-----+-----+-----+-----+-----+-----+-----+

当我尝试“手动”执行此操作而不使用 year_manager 过程时,输出没问题:

iex(1)> month1 = spawn(ConcCalendar.Formatter, :month_manager, [1, 2021, [], []])
#PID<0.241.0>
iex(2)> send month1, {:create_days}
{:create_days}
iex(3)> send month1, {:create_printable}
{:create_printable}
iex(4)> send month1, {:set_printable}
{:set_printable}
iex(5)> send month1, {:print}
{:print}
+-----------------------------------------+
|                 January                 |
+-----+-----+-----+-----+-----+-----+-----+
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
+-----+-----+-----+-----+-----+-----+-----+
|     |     |     |     | 1   | 2   | 3   |
| 4   | 5   | 6   | 7   | 8   | 9   | 10  |
| 11  | 12  | 13  | 14  | 15  | 16  | 17  |
| 18  | 19  | 20  | 21  | 22  | 23  | 24  |
| 25  | 26  | 27  | 28  | 29  | 30  | 31  |
+-----+-----+-----+-----+-----+-----+-----+

如果我尝试取消注释第一个模块中 {:print} -> 块中的行,我也会收到错误消息,这意味着我试图在最后一个模块中发送 {:create_printable}{:set_printable} 消息, {:print}.

欢迎任何提示,包括那些与我的问题没有直接关系的提示。我只是一个厌倦了无处不在的 OOP 的 CS 学生,在我的大学学习了 Erlang 课程后,我尝试了 Elixir。

我知道只用生成过程来做所有事情并不是最好的方法,但这是我的第一个 Elixir 项目,我现在更愿意继续使用它。

① 在消息传递中不需要用元组包裹单个原子,任何术语都适用
② 参考文献provided by Adam in the comments绝对值得一读,全书
③ 如果您将收到的错误消息与已通过的未注释消息分享,那将非常有帮助


问题是 Kernel.send/2 sends is normal message, :info in 术语,它是异步的。

每个进程都有一个邮箱,在调用 receive/1 时会逐条处理邮件。在进程内部,所有代码都是同步执行的。因此,您的 :print 处理程序基本上不会按照您的想法去做,而是在 VM 有机会传递消息之前向 self() 发送几条消息并立即执行 print_month/3到流程。

我建议不要为所有事情都发送消息,而是按照需要完成的同步部分:使用函数。有点像这样。

create_printable = fn ->
  # code
end

set_printable = fn ->
  # code
end

receive do
...
  :create_printable ->
    create_printable() # if still needed
  :set_printable ->
    set_printable()    # if still needed
  :print ->
    create_printable()
    set_printable()
    print_month(printable, month, year)
    month_manager(month, year, days, printable)
end