lurodrigo

lurodrigo

I’m refactoring some parts of a codebase that do some compile-time magic, in order to reduce the number of compile-time dependencies between modules. One thing I don’t fully get is when nominal references to other modules/aliases in module attributes create a compile-time dependency.

I have a minimal example here (compiling with elixir 1.15.7-otp-26)

If I have these two modules:

defmodule Trace.Submodules.Submodule do
end

and

defmodule Trace do
  alias Trace.Submodules

  @attribute Submodules.Submodule
  @attribute Submodules.DoesNotExist
  @attribute [Submodules.Submodule]
  @attribute [Submodules.DoesNotExist]
  @attribute to_string(Submodules.Submodule)
  @attribute to_string(Submodules.DoesNotExist)
  @attribute to_string(Submodules.Submodule) <> to_string(Submodules.DoesNotExist)
  @attribute (& &1).(Submodules.Submodule)

  def attribute, do: @attribute
end

When I run mix xref trace lib/trace.ex --label compile it shows Trace three places where there are compile-time dependencies on Trace.Submodules.Submodule → precisely where there are function calls wrapping Subodules.Submodule, but not a direct reference or just wrapping in a list.

lib/trace.ex:8: alias Trace.Submodules.Submodule (compile)
lib/trace.ex:10: alias Trace.Submodules.Submodule (compile)
lib/trace.ex:11: alias Trace.Submodules.Submodule (compile)

It’s weirder because I do the exact same operations for Submodules.DoesNotExist, which doesn’t exist, and there’s no problem. Is it the case the compiler assumes the alias can be used as a module by the function at compile-time, so it places a compile-time dependency on it?

Asking bc the reason behing the behaviour impacts how I’ll approach refactoring. Any help appreciated!

Showing Posts 1 to 4

xpg

xpg

Unfortunately, I can’t help you, but I am running into the exact same issue.
I have noticed that setting a module attribute with @ behaves differently when it comes to compile-time dependencies than using Macro.expand_literals and Module.put_attribute/3. The latter will add a compile-time dependency, while the former does not.

Imagine the following silly code:

defmodule HavingFun.A do
  alias HavingFun.B
  import HavingFun.C
  set_impl B
  def run(), do: @impl_module2.my_magic_function(42)
end

defmodule HavingFun.C do
  defmacro set_impl(impl) do
    quote do
      @impl_module2 unquote(impl)
    end
  end
end

Doing mix xref trace lib/having_fun/a.ex yields the following:

lib/having_fun/a.ex:4: require HavingFun.C (export)
lib/having_fun/a.ex:6: import HavingFun.C.set_impl/1 (compile)
lib/having_fun/a.ex:8: call HavingFun.B.my_magic_function/1 (runtime)

Which makes perfect sense to me. However, if the macro implementation is changed slightly, we end up having a compile-time dependency on B:

defmodule HavingFun.C do
  defmacro set_impl(impl) do
    impl = Macro.expand_literals(impl, __CALLER__)
    Module.put_attribute(__CALLER__.module, :impl_module2, impl)

    []
  end
end
lib/having_fun/a.ex:4: require HavingFun.C (export)
lib/having_fun/a.ex:6: alias HavingFun.B (compile)
lib/having_fun/a.ex:6: import HavingFun.C.set_impl/1 (compile)
lib/having_fun/a.ex:8: call HavingFun.B.my_magic_function/1 (runtime)

Line 6 is where set_impl is invoked. It seems that expand_literals adds a compile-time dependency, which doesn’t really makes any sense to me, as we are only referencing the module, and nothing specific in it.

So maybe there is a special case somewhere, that ensures that no compile time dependency is added when using @attribute <MODULE> ?

sodapopcan

sodapopcan

I don’t have the most in depth mental model, but I did spend the better part of a week eliminating a bunch of compile-time dependencies from a mediumish codebase at my old job so learned a thing or two.

To me, it makes sense that the functional call one would cause a dependency. This is because you are calling a function in a module body which means the function will be run at compile time. Because you’re passing an atom as an argument, my somewhat informed assumption is that the compiler now needs to check if that atom references a module or not and compile it if it is. This is because the function receiving it, which again is being called at compile time, might try and call one of its functions.

I think this behaviour of bare modules references inside a module body not causing a compile time dependency might be a more recent improvement. I believe this was a major cause of the problems in the codebase I worked on, but I can’t quite remember as it’s coming up on two years ago that I worked on it. I could be wrong on either count! I do remember some chatter on the forum about this in the past year, specifically around modules in guards (which is essentially the module body) but again, memory is fuzzy.

Sorry if I wasn’t more helpful. I saw this when you first posted it and just responding now since you got no bites.

And sorry I don’t have any insights for you @xpg—those macros are a little too mind-bending for me this morning (even if they are probably simple enough, lol).

xpg

xpg

That explanation makes sense to me. Good analysis!

Applying your way of thinking (and a bit looking and xref source code) also turned out to explain and solve my situation: Macro.expand_literals/2 will indeed add compile-time dependencies if the provided environment (__CALLER__ in my case) represents the body of a module. But it will only add a runtime dependency, if the environment represents the body of a function.
The documentation of Macro.expand_literals/2 hints that you can just modify the environment passed in, such that the code “thinks” you are in a function body. Unfortunately, I didn’t quite understand the hint for quite some time :slight_smile:

lurodrigo

lurodrigo OP

Thanks for the discussion.

In the end, I decided that in my case it would be better to replace direct module references with names (say, :integer instead of MyApp.Type.Integer) and have a registration layer where the association between names and modules is resolved. Besides avoiding compile-time dependencies, it also adds some flexibility as it allows switching implementations in runtime/tests.

Of course having more tricks available is helpful depending on the case!

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews