Is data copied with inter-process closures?

Hello, this is a question about processes and sharing data.

I know that when a message is sent to a process, the data is copied. So, for example, if I do this:

data = %{foo: 1}

handler = fn(next) ->
  receive do
    data ->
      IO.puts("received: #{inspect(data)}")
      next.(next)
  end
end

pid = spawn(fn -> handler.(handler) end)

send(pid, data)

Then the data map is copied when sent.

But what about doing this instead:

data = %{foo: 1}

f = fn ->
  IO.puts("closures ftw: #{inspect(data)}")
end

spawn(f) # 1
f.()     # 2

Both 1 and 2 produce the same result, and my guess is that nothing is copied for 2. What about 1 though? What happens when the function is run in another process?

It has to be copied because each process has a shared-nothing heap. The original process heap can garbage collect anything that is no longer reachable in the current process.

2 Likes

Because anonymous functions are closures, they keep track of the data captured from outside the function definition. In other words, each fun has an associated data table containing an entry for each variable referenced inside the function definition along with the value for that variable.

So when a function is passed to spawn(), the runtime system knows what it needs to copy into the newly spawned process in order to run the function there.

iex(8)> data = "data"
iex(9)> num = 13
iex(10)> foo = fn -> IO.puts "this is my data: #{inspect(data)} and number: #{inspect(num)}" end
#Function<20.128620087/0 in :erl_eval.expr/5>
iex(11)> :erlang.fun_info foo
[
  pid: #PID<0.106.0>,
  module: :erl_eval,
  new_index: 20,
  new_uniq: <<245, 82, 198, 227, 120, 209, 152, 67, 80, 234, 138, 144, 123, 165,
    151, 196>>,
  index: 20,
  uniq: 128620087,
  name: :"-expr/5-fun-3-",
  arity: 0,
  env: [
    # The first list below is the data from external variables stored inside
    # the closure.
    {[_@0: "data", _@2: 13], :none, :none,
     [
       {:clause, 10, [], [],
        [
          {:call, 10, {:remote, 10, {:atom, 0, IO}, {:atom, 10, :puts}},
           [
             {:bin, 10,
              [
                {:bin_element, 10, {:string, 0, 'this is my data: '}, :default,
                 :default},
                {:bin_element, 10,
                 {:call, 10,
                  {:remote, 10, {:atom, 0, Kernel}, {:atom, 10, :inspect}},
                  [{:var, 10, :_@0}]}, :default, [:binary]},
                {:bin_element, 10, {:string, 0, ' and number: '}, :default,
                 :default},
                {:bin_element, 10,
                 {:call, 10,
                  {:remote, 10, {:atom, 0, Kernel}, {:atom, 10, :inspect}},
                  [{:var, 10, :_@2}]}, :default, [:binary]}
              ]}
           ]}
        ]}
     ]}
  ],
  type: :local
]

Here’s another example:

iex(14)> my_string = "this is a string inside a fun"
iex(15)> bar = fn -> String.reverse(my_string) end
#Function<20.128620087/0 in :erl_eval.expr/5>
iex(16)> :erlang.fun_info bar
[
  pid: #PID<0.106.0>,
  module: :erl_eval,
  new_index: 20,
  new_uniq: <<245, 82, 198, 227, 120, 209, 152, 67, 80, 234, 138, 144, 123, 165,
    151, 196>>,
  index: 20,
  uniq: 128620087,
  name: :"-expr/5-fun-3-",
  arity: 0,
  env: [
    {[_@3: "this is a string inside a fun"], :none, :none,
     [
       {:clause, 15, [], [],
        [
          {:call, 15, {:remote, 15, {:atom, 0, String}, {:atom, 15, :reverse}},
           [{:var, 15, :_@3}]}
        ]}
     ]}
  ],
  type: :local
]

Compare that to a fun that doesn’t close over any data external to it:

iex(2)> :erlang.fun_info fn -> str = "just a string"; String.reverse(str) end
[
  pid: #PID<0.106.0>,
  module: :erl_eval,
  new_index: 20,
  new_uniq: <<245, 82, 198, 227, 120, 209, 152, 67, 80, 234, 138, 144, 123, 165,
    151, 196>>,
  index: 20,
  uniq: 128620087,
  name: :"-expr/5-fun-3-",
  arity: 0,
  env: [
    {[], :none, :none,
     [
       {:clause, 2, [], [],
        [
          {:match, 2, {:var, 2, :_str@1},
           {:bin, 0,
            [
              {:bin_element, 0, {:string, 0, 'just a string'}, :default,
               :default}
            ]}},
          {:call, 2, {:remote, 2, {:atom, 0, String}, {:atom, 2, :reverse}},
           [{:var, 2, :_str@1}]}
        ]}
     ]}
  ],
  type: :local
]
5 Likes

Thank you both for the answers. :erlang.fun_info/1 makes it very clear!

1 Like