Skip to content

Instantly share code, notes, and snippets.

@kikofernandez
Created October 5, 2022 09:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kikofernandez/92e53363f860bc4d98ac66c334f896d0 to your computer and use it in GitHub Desktop.
Save kikofernandez/92e53363f860bc4d98ac66c334f896d0 to your computer and use it in GitHub Desktop.
EEP Deprecation of function arguments to Erlang
Author: Kiko Fernandez-Reyes <kiko@erlang.org>
Status: Draft
Type: Standards Track
Created: 10-Oct-2022
Post-History:
Replaces: 2, 3

EEP 61: Deprecation of types in functions

Abstract

This EEP proposes a mechanism for deprecating type signatures from functions. This deprecation information must be integrated with Dialyzer and erl_doc.

Rationale

Erlang programmers may uses type specs with four different purposes:

  1. To generate various forms of documentation (see XXXX)
  2. To write the expected input and output of a function in code
  3. To help Dialyzer check that the expected inferred value matches the type specs
  4. 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.

Motivating example

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:

  1. 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
  2. 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
  3. Dialyzer could complain that a return of type essldoes not match the type spec
  4. External tools (type checkers) will reject programs that may not have removed the essl atom from code.

Proposal

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.

Dialyzer

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

erl_doc

Generates an XML note in the corresponding function warning about its deprecation.

DEPRECATIONS

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.

Other approaches

Erlang deprecated and removed functions

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

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:

Python

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

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

By-Hand

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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment