I have a Sequence
struct that consists of a state
and a generator
function which generates the new state from the old one. I want to write a limit
function that returns a new sequence that should return the new state exactly n
times at max and every n + k
time it should return nil
. The code so far is:
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
I want to have the following results:
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
The problem is that the IO.puts
prints always the same number, meaning it doesn't change. However my limit generator depends on that value and it changing in the closure. What is the problem here and how can I fix it ? Any help is welcomed :)
PS: I'm not allowed to add new fields to the structure and I don't want to use things like GenServer
and ETS
For elixir, closures are also extremely common, and often people who are new to the language get confused as to how they work. So, in this blog, I will explain with some examples which will hopefully make things clearer as to how function scopes and closures work in elixir.
A closure refers to the value of something (like a variable or a function) in an outer scope being available in the context of the inner scope, even if that inner scope is invoked much later.
A closure as a first-class function in C# A closure is a particular type of function that is intrinsically linked to the environment in which it is referenced. As a result, closures can use variables pertaining to the referencing environment, despite these values being outside the scope of the closure.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function.
In most cases, it makes sense to create an MCVE to locate a problem and understand what’s going on. Let’s do it:
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
OK, let’s test it:
iex|2 ▶ Test.closure(2).(2)
2
1
NIL
:ok
Cool, it works as expected. Now let’s turn back to your code:
st ->
IO.puts(n) # no closure happens here
n = n - 1
generator.(st)
The second line in the clause has no effect at all, since everything in Elixir is immutable. n = n - 1
rebounds local variable n
to the new value, but it’s dropped (GC’d) immediately after, since generator
receives st
and n
is not used anymore anywhere.
The code is quite cumbersome, but I would suggest you don’t need to accumulate the current n
in create_limit_gen
, what you see currently is exactly how closures work: n
is being assigned once, when a closure was created, and it does not change along time. To change it, one should change it explicitly, e.g. by passing n
through (as shown in my first snippet for MCVE.)
Something like
generator.(n, create_limit_gen(generator, n - 1))
and proper handling the result should do the trick.
I am not sure what exactly you want to accomplish but I think it would be better to do it like this:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With