Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create closure in Elixir

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

like image 382
user3719857 Avatar asked Apr 02 '17 22:04

user3719857


People also ask

Does elixir have closures?

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.

What is a closure 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.

What is a closure in c#?

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.

What is a Javascript 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.


2 Answers

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.

like image 185
Aleksei Matiushkin Avatar answered Oct 14 '22 00:10

Aleksei Matiushkin


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
like image 44
D. Andersen Avatar answered Oct 14 '22 00:10

D. Andersen