Создать закрытие в Эликсире
У меня есть структура 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