defp functions callable via apply/3 despite compiler warnings
0 reputation · 16 Mar 2021, 17:29 UTC
Private function convention vs. runtime reality
Elixir's defp signals internal API intent and triggers compile-time warnings when referenced across module boundaries. However, the BEAM runtime exports all functions—including those marked private—so Kernel.apply/3, :erlang.apply/3, and Module.concat/2 can invoke them without error. This gap between static analysis and dynamic behavior creates uncertainty when designing internal interfaces that must not be depended upon by other applications in an umbrella or by external dependencies.
Constraints and open questions
- Compiler warnings are suppressed inside macro expansions and generated code, allowing silent bypasses.
- Hot code upgrades may leave old callers invoking private functions that newer versions have removed.
- No built-in "internal" visibility scope exists between apps in the same workspace; umbrella applications expose all modules publicly.
- Tooling such as Dialyzer and Credo can surface cross-module private calls but cannot prevent them at runtime.
Given that the core team treats encapsulation as convention rather than enforcement, what patterns reliably communicate and defend internal API boundaries in a mixed-codebase environment? How do teams verify that no external caller—generated code, tests, or dependencies—relies on a defp function before refactoring or removing it? What lightweight conventions or lint rules have proven effective without requiring whole-program analysis?