/otp_compat

Types and Definitions supporting projects running on multiple Erlang/OTP versions.

Primary LanguageErlangApache License 2.0Apache-2.0

OTP Compatibility Goodies

This repository exists to provide a small dependency that can be included in Rebar-based Erlang/OTP projects to access features in a version-independent manner. Obviously, anything you can do in rebar, you can do without it, but my goal is ease-of-use in rebar.

If you're building without rebar, you can find an example of one way to handle getting the OTP release in this project's Makefile.

The general philosophy is to provide a common API across versions, starting with (but not limited to) the global types that have moved into namespaces as of OTP-17. For example, the dict type has moved to the dict namespace and is deprecated in the global namespace. Using this package, you can use that type as dict_t regardless of the OTP version you're building with (see below).

In addition to type mapping, there are other parts of the Erlang/OTP API that have changed in recent versions, and I'm adding them as they cause me pain. One of those, the move in the crypto API from distinct per-algoritm digest functions to a set of common hash functions, is partially addressed in the crypto_hash.hrl file, which defines macros that expand to the appropriate functions at compile time (another reason I may move a bunch of this into a parse_tranform as mentioned below).

This work is published under an Apache license. The owner of the copyright may change, the license terms will not.

I've taken the approach of defining a macro to turn off current features instead of using one (like the somewhat-common namespaced_types approach) to turn them on, on the assumption that once the target code evolves to the point that it no longer supports OTP releases without the features, the macro no longer needs to be defined at all.

Include the following entries in your rebar.config file to turn on support for older versions of OTP:

{erl_opts, [
    . . .
    % Include to use the xxx_t type definitions:
    {platform_define, "^R[1-9]", no_otp_namespaced_types}
    % Include to use the ?crypto_hash_xxx macros:
    {platform_define, "^R1[0-5]", no_otp_crypto_hash}
    . . .
]}.

{deps, [
    . . .
    {otp_compat, ".*", {git, "git://github.com/tburghart/otp_compat.git", {branch, "master"}}}
    . . .
]}.

Do not use any branch other than master unless you want much pain and suffering ... you have been warned!

Then, in your Erlang source, include the following line to make the target types accessible as typename_t (see below).

-include_lib("otp_compat/include/ns_types.hrl").

The _t suffix was chosen not because I want to make your Erlang code look like C, but because it seemed like a pattern that would be a) easy to remember and b) unlikely to conflict with existing Erlang code, which generally eschews C-like conventions.

To use the crypto_hash_xxx macros, you'd put the following in your source

-include_lib("otp_compat/include/crypto_hash.hrl").

then use (for example) ?crypto_hash_sha(...) where you had previously used crypto:sha(...) or crypto:hash(sha, ...) to get the correct function for the version you're compiling with.

The macro ?NAMESPACED_TYPES_LIST is defined as a list of the types declared in the ns_types.hrl file in a form suitable for use in -export_type(). As it seems likely that the list will be pretty static, they are reproduced here:

  • array_t/0
  • array_t/1
  • dict_t/0
  • dict_t/2
  • digraph_t/0
  • gb_set_t/0
  • gb_set_t/1
  • gb_tree_t/0
  • gb_tree_t/2
  • queue_t/0
  • queue_t/1
  • set_t/0
  • set_t/1
  • tid_t/0

Along with type definitions, a short list of (possibly helpful) functions are exposed. The public API documentation is generated by running make docs. Generally, the most interesting of these functions to users not rummaging around in the weeds will be otp_compat:otp_version(), which returns the current OTP major release as an integer, saving you the annoyance of parsing the variety of formats returned by erlang:system_info(otp_release).

Dialyzer prior to R16 will fail due to what it perceives to be duplicate type definitions for parameterized types. The Erlang compiler has no such problem, and since the target audience for this package is developers using newer, not older, versions of Erlang, I've chosen not to bother adding more macros to get around it.

If you come across issues other than the above, please do file them, and I'll have a look.

Ideally, I'll be able to work out the details of making the types available dynamically at runtime based on the running OTP release before OTP-18 (which removes them from the global namespace entirely) sees wide adoption, but that code's not ready for prime time quite yet. In the interim, code compiled with OTP-R16 or earlier may not work properly on OTP-18, and code compiled with OTP-17 or later may not work properly on OTP-R16 or earlier.

Unless you're rooting about in beam file internals, you're unlikely to stumble across any problems with the current static compile-time typing, even across OTP versions. Dialyzer, however, does just that, so if you compile a beam on one side of the type change boundary and run dialyzer from the other side of the boundary on it, expect copious warnings (see Issues about outright dialyzer breakage in older versions). Realistically, dialyzer is generally a build step, so it should be in sync, but if you're running it against sources with the --src option be sure no_otp_namespaced_types is defined (or not) as it is for compilation.

I'm also leaning toward making the whole thing a parse_tranform, which will allow me to add new capabilities without changing target source code at all. Among those capabilities would be options selecting whether the result of the transformation should be targeted at only the OTP version on which the compilation is performed (no performance impact on mapped functions), or on any OTP version (some performance impact using dynamically-mapped functions). I believe that a lot of the potential performance impact can be alleviated using some of Erlang's nifty dynamic code loading capabilities, but it'll probably be a while before I have the time to dig into that.

The possibilities are not technically endless, but there's far more that can be done transforming the intermediate forms than I can do with static macros and type definitions alone.

This is very much a work in progress, More to Come ...