Создать закрытие в Эликсире

У меня есть структура Sequence, состоящая из функции state и generator, которая генерирует новое состояние из старого. Я хочу написать функцию limit, которая возвращает новую последовательность, которая должна возвращать новое состояние в точности n times at max и каждые n + k время, которое оно должно вернуть nil. Код до сих пор:

defmodule Sequence do
  defstruct [:state, :generator]

  def generate(%Sequence{state: nil}) do
    nil
  end

  def generate(%Sequence{state: state , generator: generator } = seq) do
    {old_state, new_state} = generator.(state) 
    { old_state,  %Sequence{ seq | state: new_state } }
  end

  def limit(%Sequence{ generator: generator } = seq, n) when n > -1 do
    lim_gen = create_limit_gen(generator, n)
    %Sequence{ seq | generator: lim_gen }
  end

  defp create_limit_gen(generator, n) do
    lim_gen = fn 
                  nil -> 
                    nil
                  _ when n == 0 -> 
                    nil
                  st ->
                    IO.puts(n) # no closure happens here
                    n = n - 1
                    generator.(st)
              end
    lim_gen
  end

end

Я хочу получить следующие результаты:

iex> seq = %Sequence{state: 0, generator: &{&1, &1 + 1}} |> Sequence.limit 2
iex> {n, seq} = seq |> Sequence.generate; n
0
iex> {n, seq} = seq |> Sequence.generate; n
1
iex> seq |> Sequence.generate
nil
iex> seq = %Sequence{state: 0, generator: &{&1, nil}} |> Sequence.limit 2
iex> {n, seq} = seq |> Sequence.generate; n
0
iex> seq |> Sequence.generate
nil

Проблема в том, что IO.puts печатает всегда одно и то же число, то есть оно не изменяется. Однако мой лимит-генератор зависит от этого значения, и он меняется в закрытии. В чем проблема и как ее исправить? Любая помощь приветствуется:)

PS: мне не разрешено добавлять новые поля в структуру, и я не хочу использовать такие вещи, как GenServer и ETS

Ответ 1

В большинстве случаев имеет смысл создать MCVE, чтобы найти проблему и понять, что происходит. Давайте сделаем это:

iex|1 ▶ defmodule Test do
...|1 ▶   def closure(n) do
...|1 ▶     fn  
...|1 ▶       _ when is_nil(n) or n == 0 -> IO.puts("NIL")
...|1 ▶       _ ->
...|1 ▶         IO.puts(n)
...|1 ▶         closure(n - 1).(n - 1) # or something else
...|1 ▶     end 
...|1 ▶   end 
...|1 ▶ end

ОК, давайте протестируем его:

iex|2 ▶ Test.closure(2).(2)  
2
1
NIL
:ok

Прохладный, он работает так, как ожидалось. Теперь вернемся к вашему коду:

st ->
  IO.puts(n) # no closure happens here
  n = n - 1
  generator.(st)

Вторая строка в разделе не имеет никакого эффекта вообще, поскольку все в Эликсире неизменно. n = n - 1 восстанавливает локальную переменную n до нового значения, но ее отбрасывается (GCd) сразу после, так как generator получает st и n больше не используется.

Код довольно громоздкий, но я бы посоветовал вам не накапливать текущий n в create_limit_gen, то, что вы видите сейчас, точно так же, как работают замыкания: n присваивается один раз, когда закрытие было и он не меняется с течением времени. Чтобы изменить его, нужно его явно изменить, например. пройдя n через (как показано в моем первом фрагменте для MCVE.)

Что-то вроде

generator.(n, create_limit_gen(generator, n - 1))

и правильное обращение с результатом должно сделать трюк.

Ответ 2

Я не уверен, что именно вы хотите достичь, но я думаю, что было бы лучше сделать это следующим образом:

def limit(seq, n) when n > -1 do
  %Sequence{state: {seq.state, n}, generator: create_limit_gen(seq.generator)}
end

defp create_limit_gen(generator) do
  lim_gen = fn
    {nil, _} ->
      nil 
    {_, 0} -> 
     nil
    {state, n} ->
      {old_state, new_state} = generator.(state)
      {old_state, {new_state, n}}
  end
  lim_gen
end