I am trying to understand why transitive compile-time dependency is there, and what’s a good strategy to remove it. The mix app for example below is available here.
In a nutshell, I have these 3 modules:
# a.ex
defmodule A do
import B, only: [valid_string: 1]
def check(string) when valid_string(string), do: "OK"
end
# b.ex
defmodule B do
defguard valid_string(string) when string in ~w(read write)
def by_the_way do
C.something()
end
end
# c.ex
defmodule C do
def something do
IO.puts("Just passing by")
end
end
Dependencies from this module form a what’s known as transitive compile-time dependency. This can be confirmed by running mix href command:
mix xref graph --label compile-connected
Output:
lib/a.ex
└── lib/b.ex (compile)
If I remove by_the_way function from module B, mix xref no longer reports a transitive compile-time dependency. Could someone explain to me why A transitively depends on C, even though A is only interested in importing the macro from B, and doesn’t really care about the function by_the_way or its implementation?
What’s a good strategy to remove such transitive compile time dependency? Is it like separating macros and functions in different modules?
That’s a quite a few assumptions about the knowledge of the compiler. In this specific case the imported functionality is a guard, which indeed cannot depend on by_the_way due to limitations of guards. But it could also be a normal macro, which might call by_the_way at some point during execution.
Because compiler do not know (and in general sense it cannot know) that A do not depend on B.by_the_way. Phoenix generates module that is perfect example of that:
def module MyAppWeb do
defmacro __using__(name) do
apply(__MODULE__, name, [])
end
# …
end
This shows that calling macro can call any function within our module.
It can:
defmodule Foo do
defguard is_foo(a) when foo(a)
defmacro foo(a) do
by_the_way()
quote do: true
end
def by_the_way, do: C.something()
end
Good point. A macro could be part of the defguard and that macro can then call anything again.
defmodule Foo do
defguard is_foo(a) when foo()
defmacro foo() do
if by_the_way() do
quote do: true
else
quote do: false
end
end
def by_the_way, do: C.something()
end
Elixir tracks dependencies at the module level. One common approach to address this is to define constants (which you may access at compile-time, such as invoice_states) and guards that are used across multiple modules into a separate module that only defines constants/guards (and does not call anything else).