Referential transparency

From what I understand, Elixir does not have referential transparency. I was still curious about it, though.

I discovered :erts_debug.same/2 in this reply which tells you if two pieces of data point to the same memory location or not. So I did a couple of tests:

iex(1)> defmodule Fry, do: def about, do: %{name: "Philip"}
iex(2)> fry = Fry.about()
iex(3)> also_fry = Fry.about()
iex(4)> :erts_debug.same(fry, also_fry)
true    # šŸ¤”

And then in a second test:

iex(1)> defmodule Employee, do: def about(name), do: %{name: name}
iex(2)> leela = Employee.about("Leela")
iex(3)> also_leela = Employee.about("Leela")
iex(4)> :erts_debug.same(leela, also_leela)
false   # šŸ§

The second example has me pretty dang satisfied that there is no referential transparency in Elixir (not that I didnā€™t believe itā€¦), but whatā€™s going on in the first one? Is that the compiler inlining the function?

Iā€™m not at my computer now, but I recommend checking :beam_disasm.file(module). The map in the first one is almost certainly a literal stored as in a single place, so two calls will return the same reference to that literal.

The leela function probably newā€™s a new map each time itā€™s called, so thereā€™s no way for the BEAM to know if they should be the same, since it could have been passed anything, let alone a string.

Thanks for the reply!

Iā€™m playing around with :beam_disasm.file but canā€™t seem to get it to return anything other than

{:error, :beam_lib,
 {:not_a_beam_file, "_build/dev/lib/disasm_test/ebin/Elixir.DisasmTest.beam"}}

(Iā€™ve tried several other inputs including bare code, code as strings, and different file paths)

What you are describing is what I assume is happening, but I was wondering how exactly it makes those decisions. Iā€™m assuming the compiler can tell if the body of a function is static or not, but I just wanted to know for sure whatā€™s going on. Iā€™m guessing :beam_disasm can help me with this! The docs are a little sparse, but Iā€™ll keep plugging away at it.

You should pass the actual binary of the file and not the path to the file. So, File.read! that path!

1 Like

Ohhhh, cool! ā€¦and I now realize ā€œdisasmā€ means ā€œdisassembleā€, doi.

Thanks for this!

https://hexdocs.pm/elixir/1.12/Macro.html#quoted_literal?/1

2 Likes

Elixir does not have referential transparency but I donā€™t think those examples prove or deny it.

Referential transparency means the compiler is able to inline those calls - but the compiler doesnā€™t necessarily have to. After all, you can think some calls may actually perform long running computations that would make them expensive to inline.

As you likely know, for a system to be referentially transparent, the compiler (and the developer) need to be able to recognize and separate code with side-effects from pure code.

Elixir and Erlang does not really tag functions with side-effects. The compiler also doesnā€™t look across modules, because of hot code reloading, so we canā€™t peak inside either. But the Erlang compiler does optimize literals and several functions from Erlangā€™s stdlib if those particular functions are guaranteed to be pure.

1 Like

Awesome, thanks for all that! That helps a bunch, especially around if the compiler is able to tag something as having side-effects or not. I donā€™t know a lot about compilers but am slowly learning a thing or two and I just like to know these things. I am still a little curious how the first example knows to point to the same memory after a function call, unless :erts_debug.same/2 doesnā€™t do exactly what I think it does.

The answers above explain it. It is tagged as a literal and all references to it point to the same place in memory.

3 Likes

Ah ok! I thought you were only talking functions from the standard lib so was a little confused but I got it now. Thanks so much for taking the time to explain!