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!