[erlang-questions] Using -spec for callbacks when defining behaviours
Kostis Sagonas
kostis@REDACTED
Wed Mar 17 16:41:54 CET 2010
Mikael Karlsson wrote:
> Hi,
>
> I understand that -spec (and -type) constructs may be used in include
> (.hrl) files and the module itself when specifying functions.
>
> Has there been any discussion on beeing able to use them in behaviour
> defining modules to specify the "callback" modules in more detail?
>
> When defining a behaviour today you "just" specify the function names and
> their arity with the behaviour_info/1 function.
>
> I think it would be good to be able to also specify the types of the
> arguments and their return type for the module implementing the behaviour.
> This would make a behaviour defining module closer to a interface
> specification and also decrease the dependencies of include files.
Absolutely!
In fact, we have had similar thoughts to yours and have been working for
about half a year now on extending Erlang to do exactly what you are
hinting at in your mail. The good news is that it's all done and
finished (dialyzer already supports this) and for about a month now
we've even written an EEP for it, but I've been so swamped that I've not
found time to properly send it to those responsible for EEPs.
I am including it below -- comments welcome,
Kostis
PS. Can I also please ask the folks of the Erlang/OTP team to put this
in the EEP homepage?
============================================================================
EEP: XXX
Title: Behaviour Specifications
Version: $Revision: 1 $
Last-Modified: $Date: Mon Mar 8 12:14:35 EET 2010$
Author: Stavros Aronis [aronisstav(at)gmail(dot)com], Kostis Sagonas
[kostis(at)cs(dot)ntua(dot)gr
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 03-Feb-2010
Erlang-Version:
Post-History:
Abstract
=======
We describe a small, backwards compatible change for behaviours to
specify the types which are expected of their callbacks.
Rationale
=======
In Erlang a behaviour is usually a complete design pattern implemented
in a module. Users can use these design patterns easily, writing in some
other module just the callback functions required to provide the
behaviour's functionality. As all these callbacks are called somewhere
within the behaviour's code, it is important to check that behaviour
callbacks have the functionality which is expected from them. Currently,
not only is this the responsibility of the programmer but this checking
is not even semi-automatic as there is no formal way for the module
defining the behaviour to specify what is expected from its callback
module. Instead, the programmer must check manually if the callback
functions accept the correct arguments and return the correct results,
and are indeed in accordance with the behaviour's available
documentation, which often only exists in paper form. What this EEP aims
at is to propose infrastructure which will ease the automatization of
checking that callback modules adhere to what's expected from them.
Given the wide usage of behaviours in current Erlang application
development, we believe that providing a means to include such
specification in a behaviour's implementation is useful for both the
behaviour's implementor, who can make clear what the types of the
callbacks should be and the behaviour's user who can employ tools that
check automatically for errors in the callback module's implementation.
Specification
==========
Currently, all behaviours must define a behaviour_info/1 function. Among
other pieces of information, a clause of this function enumerates (in
the form of 2-tuples) the function names and arities of the required
callbacks.
The following example is taken from the 'gen_server' behaviour:
behaviour_info(callbacks) ->
[{init,1}, {handle_call,3}, {handle_cast,2}, {terminate, 2},
{code_change, 3}].
Often, though not always, the behaviour module also contains some
additional information in the form of comments. Continuing the same
example, the gen_server module includes the following comments:
%%% The user module should export:
%%%
%%% init(Args)
%%% ==> {ok, State}
%%% {ok, State, Timeout}
%%% ignore
%%% {stop, Reason}
%%%
%%% handle_call(Msg, {From, Tag}, State)
%%%
%%% ==> {reply, Reply, State}
%%% {reply, Reply, State, Timeout}
%%% {noreply, State}
%%% {noreply, State, Timeout}
%%% {stop, Reason, Reply, State}
%%% Reason = normal | shutdown | Term terminate(State) is called
%%%
%%% .... MORE COMMENTS FOR THE OTHER THREE CALLBACKS HERE .....
The problem with comments is that they are in free text form, often
lacking some information as in the case above, and cannot be
mechanically processed.
We propose a modification in the behaviour_info(callbacks) clause so
that it also specifies the types which are expected from these callbacks.
The modification itself is very simple, adding a third element in the
tuple list.
The third element is a string whose contents is a -spec in the already
existing language of EEP8:
behaviour_info(callbacks) ->
[{init, 1,
"-spec init(Args) ->
{'ok', State} |
{'ok', State, timeout() | 'hibernate'} |
{'stop', Reason} |
'ignore'."},"},
{handle_call, 3,
"-spec handle_call(Request, From :: {pid(), Tag}, State) ->
{'reply', Reply, NewState} |
{'reply', Reply, NewState, timeout() | 'hibernate'} |
{'noreply', NewState} |
{'noreply', NewState, timeout() | 'hibernate'} |
{'stop', Reason, Reply, NewState} |
{'stop', Reason, NewState}."},
{handle_cast, 2, "-spec ... SOME SPEC HERE ..."},
....].
A defect detection tool (e.g. dialyzer) can then use these specs as a
reference to compare the inferred types of the callbacks.
Incidentally, the above example shows various interesting things:
1) Using the language of types and specs, one can provide information
both for documentation purposes and for types as e.g. in From :: {pid(),
Tag}
2) It's not necessary to specify every type as e.g. the Tag variable
above, which is a convenient shorthand for Tag :: term()
3) Comments are often incomplete or can easily become obsolete as e.g.
the 'hibernate' value is nowhere mentioned.
A final note: One may wonder why the specs are written as strings
instead of as -specs. There are various reasons for this, though none of
them is deep or cast in stone. First of all specs are not proper Erlang
terms (they are file attributes), so they cannot be used as is in places
where Erlang terms are expected. Since we wanted to reuse as much of the
existing infrastructure of behaviours as possible, the natural place to
put them was to extend the behaviour_info(callbacks) clause. Second,
they specify a type obligation (i.e., a contract) for all the callback
modules, not for some specific known module or the behaviour module that
contains them (the behaviour module does not contain definitions for
these functions). Because of this, they cannot be placed in the
behaviour defining module since currently there is no way for the
language of specs to express something of the form: for all callback
modules M, -spec M:init(Args) -> ....
If we use a file attribute other than -spec (e.g. -callback_spec) these
specs could be part of the module defining the behaviour instead of
being part of the behaviour_info(callbacks) clause.
Implementation
==============
We have already implemented the above proposal and we can provide a
fully working implementation.
Trivial changes were needed in the compiler to accept the new
behaviour_info format when checking for the presence of all the required
callbacks in a module that uses the behaviour, as well as minor changes
in some other library modules. Using this extension of dialyzer in a
significant corpus of Erlang code we have already detected many
violations of the published behaviour documentation.
Backwards Compatibility
=======================
The extension is fully backwards compatible. The old format is still
supported (the clause of the behaviour_info function can contain both
pairs and triples).
The inclusion of the -spec string is optional and no warnings are
emitted unless Dialyzer is asked explicitly to check the behaviour usage
via a corresponding option.
Copyright
=========
This document has been placed in the public domain.
..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
coding: utf-8
More information about the erlang-questions
mailing list