Is there a reason why we can’t have callbacks for unexported functions

Do you have a minimal example reproducing the order of magnitude difference?

To answer the original question:

First of all, Erlang doesn’t have the concept of “callback” per se. -behavior attribute only affects static checks, and it doesn’t influence code generation in any way, as far as I know. When you specify that certain module implements a behavior (for example, by writing -behavior(gen_server).), the compiler will generate warnings if the module doesn’t export functions specified using -callback attribute in the module gen_server. That’s it. Erlang behavior is not some magic language feature, it’s merely a way to specify contract of a module interface. Since un-exported functions are not module interface, the question doesn’t make much sense.

To understand how behaviors work, consider the simplest example:

-module(foo).

-export([apply_foo/2]).

%% Declare that any module implementing `foo' behavior must export function `foo/1':
-callback foo(term) -> term().

-spec apply_foo(module(), term()) -> term().
apply_foo(Module, Input) ->
  Module:foo(Input).

This is the most primitive example of a behavior module, but “real” behaviors like gen_server use the callbacks in a similar fashion. Notice three things:

  1. It’s entirely possible to omit -callback attribute, and it won’t affect how any code works in the runtime.
  2. Invoking “callback” foo is a simple function call. There’s nothing special about Mod:foo call that makes it a “callback”.
  3. Obviously, you cannot use an un-exported function in this setting, because it’s, well, not exported.

Now, regarding performance.

Different ways of invoking a function do indeed have different overhead, see Erlang -- Functions . But calling a function is relatively cheap regardless of the method. Practically speaking, you can only notice the difference in a synthetic benchmark that repeatedly calls a function that does nothing. Also notice that gen_server module “caches” some callback funs in its state. I’m not sure whether it’s done for performance or some other reason.

I can speculate that there’s only one scenario where external function calls may take a very long time to complete: when calling a non-existent module in interactive mode. Then code loader may repeatedly try to load the module, and it will involve scanning the search path and dealing with the file system. Since the only outcome of such call is raising an exception, I would argue this is a pathological case.

With this information in mind, you can write generic algorithms using internal functions: you just write a higher-order function taking local function(s) as argument(s).

1 Like