Author: Kiko Fernandez-Reyes <kiko@erlang.org>
Status: Draft
Type: Standards Track
Created: 10-Oct-2022
Post-History:
Replaces: 2, 3
This EEP proposes a mechanism for deprecating type signatures from functions. This deprecation information must be integrated with Dialyzer and erl_doc.
Erlang programmers may uses type specs with four different purposes:
- To generate various forms of documentation (see XXXX)
- To write the expected input and output of a function in code
- To help Dialyzer check that the expected inferred value matches the type specs
- To run type checkers and other 3rd party tools
This mix of concerns from above makes deprecating types in a function difficult to achieve and to warn users about.
Lets assume we want to deprecate the atom essl
in function getopts
in
inets/src/http_client/httpc.erl
.
-spec getopts(SocketType, Socket) -> Object when
SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
If one were to simply remove the essl
atom from the function, the implications
of the mix of concerns from the numbered list from above are:
- Documentation does not show unwanted spec, but the documentation does not show a change in the API type either nor a deprecation of the types used
- Code does not show the
essl
as an expected type, but the code is to be deprecated and clients of the library need some time to update their usages - Dialyzer could complain that a return of type
essl
does not match the type spec - External tools (type checkers) will reject programs that may not have removed
the
essl
atom from code.
A new language construct / keyword, -deprecate_spec
that deprecates the given spec
.
The semantics of -deprecate_spec
are the same as -spec
, except that it clearly
marks that the type spec is deprecated along with some meta-information.
If API designers want to deprecate the atom essl
in the function getopts
:
-spec getopts(SocketType, Socket) -> Object when
SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
they could do so as follows:
-spec getopts(SocketType, Socket) -> Object when
SocketType :: ip_comm | {ip_comm | ssl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
-deprecate_spec getopts(SocketType, Socket) -> Object when
SocketType :: {essl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | []
with note = "Text here",
since = "OTP-25",
removed = "OTP-26".
The difference is simply in the removal of essl
from -spec ...
and its
addition to the -deprecate_spec
. Apart from this, the -deprecate_spec
adds a
new keyword, with
, where API designers can pass meta-information about the
deprecation, such as a note
that informs users of the reason (follows the
reasoning behind Erlang's standard -deprecated
link),
since
which marks deprecated version that applies, and removal
version.
The purpose of this EEP is to add the -deprecated_spec
into Erlang
and its integration with the following Erlang tooling.
The semantics of the -deprecate_spec
should be isomorphic to -spec
, i.e.,
the specifcation:
-spec getopts(SocketType, Socket) -> Object when
SocketType :: ip_comm | {ip_comm | ssl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
-deprecate_spec getopts(SocketType, Socket) -> Object when
SocketType :: {essl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | []
with note = "Text here",
since = "OTP-25",
removed = "OTP-26".
should be regarded for all purposes as
-spec getopts(SocketType, Socket) -> Object when
SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
Socket :: ssl:sslsocket() | inet:socket(),
Object :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
except that Dialyzer needs to maintain a set of deprecated functions, so that it can throw warnings in cases where it finds usage of deprecated values.
The following options govern how Dialyzer can inform or ignore the deprecated_spec
:
Wno_deprecated_specs
, Suppress warnings about deprecated specs
Generates an XML note in the corresponding function warning about its deprecation.
Adds to DEPRECATIONS file the function that is to be removed
Dialyzer warnings, generate documentation, add deprecated and removed functionality to existing DEPRECATIONS file, etc.
Erlang has the keywords -deprecated/1
and -removed/1
to deprecate and remove
functions
(link).
These options are good to deprecate and remove whole functions, but do not warn
users about changes in type signatures that are to be deprecated nor generate
the documentation that mentions the deprecation / removal.
-deprecated([{now,0,
"see the \"Time and Time Correction in Erlang\" "
"chapter of the ERTS User's Guide for more information"}]).
-deprecated([{cmac, 3, "use crypto:mac/4 instead"},
{cmac, 4, "use crypto:macN/5 instead"}]).
-removed([{md5_mac,2,"use crypto:hmac/3 instead"},
{md5_mac_96,2,"use crypto:hmac/4 instead"}]).
-deprecated_type([{gadget,1,"use widget/1 instead"}]).
-removed_type([{column,0,"use erl_anno:column() instead"},
{line,0,"use erl_anno:line() instead"},
{location,0,"use erl_anno:location() instead"}]).
Rust uses a similar approach to this one. However, its deprecation annotation works for functions, traits, and other constructs.
extern crate rust_foo;
#[deprecated(since = "0.2.1",
note="The rust_foo version is more advanced, and this crate's will likely be discontinued")]
struct Foo { .. }
References:
There are libraries that implement this feature using the decorator pattern. Overall, they follow the same principle of marking a function as deprecated with some of its meta-information:
import deprecation
@deprecation.deprecated(deprecated_in="1.0", removed_in="2.0",
current_version=__version__,
details="Use the bar function instead")
def foo():
"""Do some stuff"""
return 1
Reference: https://deprecation.readthedocs.io/en/latest/
Java has a @Deprecated
annotation that works for constructors, fields, local
variables, methods, packages, parameters, and types as follows (definition):
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
An example (taken from Stackoverflow) follows:
interface Doable {
void doIt(@Deprecated Object input);
}
class Action implements Doable {
@Override
public void doIt(@Deprecated Object input) {
String string = String.valueOf(input); // no warning!
@Deprecated
String localVariable = "hello";
System.out.println("Am I using a deprecated variable?" + localVariable); // no warning!
}
}
Reference: Java Language Specification 19, Deprecated
One way to deprecate a type in a function is to create deprecate the current function
and copy-paste its content into the new expected function. For example, lets say we want
to remove the essl
atom of the following type spec:
-spec getopts(SSLConfig) -> Result when
SSLConfig :: ssl | essl,
Result :: integer().
getopts(SSL) ->
foo(SSL).
One could deprecate the funtion with existing Erlang functionality and create a new funtion that works as expected:
-deprecated([{getopts, 1, "use new_getopts/1 instead"}]).
-spec getopts(SSLConfig) -> Result when
SSLConfig :: ssl | essl,
Result :: integer().
getopts(SSL) ->
foo(SSL).
-spec new_getopts(SSLConfig) -> Result when
SSLConfig :: ssl,
Result :: integer().
new_getopts(SSL) ->
foo(SSL).