I was thinking about the following thing for some time and I want to hear what other people think of this.
While implementing some behavior, we must implement and export callbacks. So, the user interface and the behavior callbacks are grouped together. Exported callbacks are AFAIK not intended to be called by the user, but only by the behavior module. This, IMHO, could create confusion and unwanted behavior while trying to interact with some implementation of behavior, especially gen_statem which allows custom callback names!
Could it be possible for dialyzer to catch those misuses? E.g. if “cellphone” is a gen_statem implementation, with states create, open, etc., but its interface only contains start… So, if the user calls open or create instead of start a warning would be generated.
Can we warn the user not to interact directly with callbacks?
Or on the other hand, are there some scenarios when you want to directly call callback functions?
There is actually a broader problem here. Some functions in general need to be exported to be used for some framework (including OTP) to work but are not intended for the “end user”.
As I understand, now we are mostly relying on the state of documentation. More specifically if the export is documented use it and if not there are no guarantees.
I propose solution in a form of new attribute -internal() like -deprecated() and -removed() (although i cannot find any documentation for them) .
for example, you could mark a function as internal for use only inside some app so that tools like xref or compiler could check for correct usage:
-module(example1).
-export([f1/0, f2/0]).
-internal({f1/0, [app1, app2]}). %usage allowed only in the context of app1 and app2
f1() ->
...
or even better:
-module(example2).
-export(f2/0]).
-internal({f1/0, [app1, app2]}). %exported for usage only in the context of app1 and app2
f1() ->
...
This has already been suggested 2007 in EEP-0005 here Eep 0005 - Erlang/OTP. I still think it is an interesting feature which also could be applied to export of types. Many type specs are only intended to be used internally within the application but not by the users of the application.
thank you for pointing out EEP, I was looking into them but I managed to miss 0005 one. That being said I would prefer to declare allowed apps instead of modules (as per my proposal).
And the EEP already raised the issue of exporting to an app and proposed a way to do
that lo! these many years ago. (In fact the export_to proposal goes back to some time
before 1997.)
There is a great deal of design thought needed before the proposal can really be
extended to applications. For example, if application A defines module M and
includes application B, and m:f/a is exported to application B, but there is a
call to it from M, should that be allowed? It has never been clear to me what
makes a process “belong to” an application and whether that property can change,
but it does invite the question whether “application X may use m:f/a” is a
statement about MODULES belonging to application X (which is all I thought of
in 2007) or to PROCESSES belonging to application X.
I would’ve thought conceptually, given a process P who’s pid() was returned by application A’s start/2 callback, a process belongs to A if it’s either P or a descendant of P (i.e., if it’s “part of A’s supervision tree”), no?
This includes processes spawned from code of included applications (or any other modules, for that matter):
when running, an included application is in fact part of the primary application, and a process in an included application considers itself belonging to the primary application.
As far as I can see, there’s currently no supported mechanism to change that property. Child process C is marked as a descendet of parent P by (ab)using the group leader mechanism, but that seems to be an undocumented implementation detail, so I’d assume fiddling with group leaders to be unsupported.
(I doubt I’m telling you news here, so maybe I missed your point?)