| name | lang-erlang-library-dev |
| description | Erlang-specific library development patterns. Use when creating OTP libraries, designing public APIs with process patterns, configuring rebar3, managing application resources, publishing to Hex, or writing EDoc. Extends meta-library-dev with Erlang/OTP tooling and idioms. |
Erlang Library Development
Erlang-specific patterns for OTP library development. This skill extends meta-library-dev with Erlang/OTP tooling, process-oriented design, and ecosystem practices.
This Skill Extends
meta-library-dev- Foundational library patterns (API design, versioning, testing strategies)lang-erlang-dev- Core Erlang patterns (processes, OTP behaviors, supervision)
For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first. For foundational Erlang patterns, see lang-erlang-dev.
This Skill Adds
- Erlang tooling: rebar3 configuration, .app.src files, application structure
- Hex publishing: Package metadata, versioning, documentation
- Library idioms: Public API design, behavior callbacks, application supervision
- OTP conventions: Application structure, configuration, resource management
- EDoc: Documentation generation, type specifications, function docs
- Dialyzer: Type analysis, PLT files, type specifications
This Skill Does NOT Cover
- General library patterns - see
meta-library-dev - Core Erlang/OTP basics - see
lang-erlang-dev - Web frameworks - see framework-specific skills
- Distributed systems - see
lang-erlang-distributed-dev - Performance optimization - see
lang-erlang-performance-dev
Quick Reference
| Task | Command/Pattern |
|---|---|
| New library app | rebar3 new lib <name> |
| New OTP library | rebar3 new app <name> |
| Compile | rebar3 compile |
| Test | rebar3 eunit or rebar3 ct |
| Type check | rebar3 dialyzer |
| Generate docs | rebar3 edoc |
| Publish (dry run) | rebar3 hex publish --dry-run |
| Publish to Hex | rebar3 hex publish |
| Shell with library | rebar3 shell |
| Run tests | rebar3 do eunit, ct |
rebar3 Configuration
Basic rebar.config
{erl_opts, [
debug_info,
warnings_as_errors,
warn_export_all,
warn_unused_import,
warn_untyped_record
]}.
{deps, []}.
{project_plugins, [rebar3_hex, rebar3_ex_doc]}.
{hex, [
{doc, #{provider => ex_doc}}
]}.
{profiles, [
{test, [
{deps, [
{proper, "1.4.0"},
{meck, "0.9.2"}
]},
{erl_opts, [nowarn_export_all]}
]},
{prod, [
{erl_opts, [no_debug_info, warnings_as_errors]}
]}
]}.
{dialyzer, [
{warnings, [
unmatched_returns,
error_handling,
underspecs
]},
{plt_extra_apps, [ssl, crypto]}
]}.
{xref_checks, [
undefined_function_calls,
undefined_functions,
locals_not_used,
deprecated_function_calls,
deprecated_functions
]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
Application Resource File (.app.src)
{application, mylib,
[{description, "A library for doing X efficiently"},
{vsn, "0.1.0"},
{registered, []},
{applications, [
kernel,
stdlib
]},
{env, []},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, [
{"GitHub", "https://github.com/username/mylib"},
{"Hex", "https://hex.pm/packages/mylib"}
]},
{build_tools, ["rebar3"]},
{files, [
"src",
"include",
"rebar.config",
"rebar.lock",
"README.md",
"LICENSE"
]}
]}.
OTP Application (.app.src with supervisor)
{application, mylib,
[{description, "OTP library with supervision tree"},
{vsn, "0.1.0"},
{registered, [mylib_sup]},
{mod, {mylib_app, []}},
{applications, [
kernel,
stdlib,
sasl
]},
{env, [
{pool_size, 10},
{timeout, 5000}
]},
{modules, []},
{licenses, ["MIT"]},
{links, [{"GitHub", "https://github.com/username/mylib"}]},
{build_tools, ["rebar3"]}
]}.
Project Structure
Simple Library (No Supervision)
mylib/
├── rebar.config
├── rebar.lock
├── README.md
├── LICENSE
├── src/
│ ├── mylib.app.src # Application resource file
│ ├── mylib.erl # Main public API module
│ ├── mylib_parser.erl # Internal module
│ └── mylib_types.hrl # Type definitions
├── include/ # Public header files
│ └── mylib.hrl
├── test/
│ ├── mylib_eunit.erl # EUnit tests
│ └── mylib_SUITE.erl # Common Test suite
├── doc/ # Generated by EDoc
│ └── overview.edoc # Documentation overview
└── priv/ # Static resources
└── templates/
OTP Application Library
mylib/
├── rebar.config
├── src/
│ ├── mylib.app.src
│ ├── mylib_app.erl # Application behavior
│ ├── mylib_sup.erl # Root supervisor
│ ├── mylib.erl # Public API
│ ├── mylib_server.erl # gen_server worker
│ └── mylib_worker.erl # Worker process
├── include/
│ └── mylib.hrl
├── test/
│ ├── mylib_eunit.erl
│ ├── mylib_SUITE.erl
│ └── mylib_prop.erl # PropEr property tests
├── doc/
│ └── overview.edoc
└── priv/
└── config/
└── defaults.config
Public API Design
Module Organization
Single entry point for users:
-module(mylib).
-export([
start/0,
stop/0,
process/1,
process/2,
format/1
]).
%% @doc Start the application
-spec start() -> ok | {error, term()}.
start() ->
application:ensure_all_started(mylib).
%% @doc Stop the application
-spec stop() -> ok.
stop() ->
application:stop(mylib).
%% @doc Process input with default options
-spec process(Input :: binary()) -> {ok, term()} | {error, term()}.
process(Input) ->
process(Input, #{}).
%% @doc Process input with custom options
-spec process(Input :: binary(), Options :: map()) ->
{ok, term()} | {error, term()}.
process(Input, Options) ->
mylib_parser:parse(Input, Options).
Behavior Definition
Define custom behaviors for extensibility:
-module(mylib_handler).
%% Behavior callback definitions
-callback init(Args :: term()) -> {ok, State :: term()} | {error, Reason :: term()}.
-callback handle_event(Event :: term(), State :: term()) ->
{ok, NewState :: term()} | {error, Reason :: term()}.
-callback terminate(Reason :: term(), State :: term()) -> ok.
-optional_callbacks([terminate/2]).
%% Export behavior
-export_type([handler/0]).
-type handler() :: module().
Implement the behavior:
-module(my_custom_handler).
-behaviour(mylib_handler).
-export([init/1, handle_event/2, terminate/2]).
init(Args) ->
{ok, #{args => Args}}.
handle_event(Event, State) ->
io:format("Received: ~p~n", [Event]),
{ok, State}.
terminate(_Reason, _State) ->
ok.
Options and Configuration
Use maps for flexible options:
-module(mylib_config).
-export([parse_options/1, defaults/0]).
-type options() :: #{
timeout => pos_integer(),
retry => boolean(),
max_retries => pos_integer(),
format => json | xml | binary
}.
-export_type([options/0]).
%% @doc Default configuration
-spec defaults() -> options().
defaults() ->
#{
timeout => 5000,
retry => true,
max_retries => 3,
format => json
}.
%% @doc Merge user options with defaults
-spec parse_options(UserOpts :: map()) -> options().
parse_options(UserOpts) ->
maps:merge(defaults(), UserOpts).
Type Specifications and Dialyzer
Complete Type Specs
-module(mylib_types).
%% Exported types
-export_type([
user_id/0,
user/0,
result/0,
error_reason/0
]).
%% Type definitions
-type user_id() :: non_neg_integer().
-type user() :: #{
id := user_id(),
name := binary(),
email := binary(),
created_at := calendar:datetime()
}.
-type error_reason() ::
not_found |
invalid_input |
timeout |
{internal_error, term()}.
-type result() :: {ok, term()} | {error, error_reason()}.
%% Records with types
-record(config, {
timeout :: pos_integer(),
max_connections :: pos_integer(),
handler :: module()
}).
-type config() :: #config{}.
-export_type([config/0]).
Function Specifications
-module(mylib_api).
%% @doc Create a new user
-spec create_user(Name :: binary(), Email :: binary()) ->
{ok, mylib_types:user()} | {error, mylib_types:error_reason()}.
create_user(Name, Email) when is_binary(Name), is_binary(Email) ->
UserId = generate_id(),
User = #{
id => UserId,
name => Name,
email => Email,
created_at => calendar:universal_time()
},
{ok, User}.
%% @doc Find user by ID
-spec find_user(mylib_types:user_id()) ->
{ok, mylib_types:user()} | {error, not_found}.
find_user(UserId) when is_integer(UserId), UserId >= 0 ->
case lookup_user(UserId) of
undefined -> {error, not_found};
User -> {ok, User}
end.
%% Internal function - no spec needed (dialyzer infers)
lookup_user(UserId) ->
ets:lookup(users_table, UserId).
Dialyzer PLT Management
% rebar.config
{dialyzer, [
{warnings, [
unmatched_returns,
error_handling,
underspecs,
unknown
]},
{plt_apps, all_deps},
{plt_extra_apps, [ssl, crypto, public_key]},
{plt_location, local},
{base_plt_location, global}
]}.
EDoc Documentation
Module Documentation
%%% @doc Main API module for MyLib.
%%%
%%% This module provides the primary interface for working with MyLib.
%%% All operations are safe to use from multiple processes concurrently.
%%%
%%% == Quick Start ==
%%%
%%% ```
%%% 1> mylib:start().
%%% ok
%%% 2> {ok, Result} = mylib:process(<<"input">>).
%%% {ok, #{data => <<"processed">>}}
%%% '''
%%%
%%% == Configuration ==
%%%
%%% The application can be configured via application environment:
%%%
%%% ```
%%% {mylib, [
%%% {timeout, 10000},
%%% {pool_size, 20}
%%% ]}
%%% '''
%%%
%%% @end
-module(mylib).
-author("Your Name <your.email@example.com>").
-copyright("2025 Your Name").
-version("0.1.0").
Function Documentation
%%% @doc Process input data with options.
%%%
%%% This function processes the input binary according to the provided
%%% options and returns the result. Processing is done asynchronously
%%% in a worker pool.
%%%
%%% == Options ==
%%%
%%% <ul>
%%% <li>`timeout' - Maximum time in milliseconds (default: 5000)</li>
%%% <li>`format' - Output format: `json' or `binary' (default: json)</li>
%%% <li>`retry' - Whether to retry on failure (default: true)</li>
%%% </ul>
%%%
%%% == Examples ==
%%%
%%% ```
%%% %% Simple processing
%%% {ok, Result} = mylib:process(<<"data">>).
%%%
%%% %% With custom timeout
%%% {ok, Result} = mylib:process(<<"data">>, #{timeout => 10000}).
%%%
%%% %% Binary output format
%%% {ok, Binary} = mylib:process(<<"data">>, #{format => binary}).
%%% '''
%%%
%%% @see process/1
%%% @end
-spec process(Input :: binary(), Options :: map()) ->
{ok, term()} | {error, term()}.
process(Input, Options) ->
% Implementation
ok.
Type Documentation
%%% @type user_id() = non_neg_integer().
%%% Unique identifier for a user.
%%% @type user() = #{
%%% id := user_id(),
%%% name := binary(),
%%% email := binary(),
%%% created_at := calendar:datetime()
%%% }.
%%% User record containing all user information.
%%% @type error_reason() =
%%% not_found |
%%% invalid_input |
%%% timeout |
%%% {internal_error, term()}.
%%% Possible error reasons returned by library functions.
Overview Documentation
Create doc/overview.edoc:
@author Your Name <your.email@example.com>
@copyright 2025 Your Name
@version 0.1.0
@title MyLib - Efficient Data Processing Library
@doc
== Overview ==
MyLib is a high-performance library for processing data in Erlang/OTP
applications. It provides a simple, consistent API while leveraging
OTP principles for reliability and scalability.
== Features ==
<ul>
<li>Concurrent processing with worker pools</li>
<li>Automatic retries and error handling</li>
<li>Multiple output formats</li>
<li>Full type specifications</li>
<li>Comprehensive test coverage</li>
</ul>
== Installation ==
Add to your `rebar.config':
{deps, [ {mylib, "0.1.0"} ]}. '''
== Quick Start ==
%% Start the application
ok = mylib:start().
%% Process some data
{ok, Result} = mylib:process(<<"input data">>).
%% Stop the application
ok = mylib:stop().
'''
@end
Testing Patterns
EUnit Tests
-module(mylib_eunit).
-include_lib("eunit/include/eunit.hrl").
%%% Setup/Teardown
setup() ->
application:ensure_all_started(mylib).
cleanup(_) ->
application:stop(mylib).
%%% Test Fixtures
mylib_test_() ->
{setup,
fun setup/0,
fun cleanup/1,
[
{"Process valid input", fun test_process_valid/0},
{"Process invalid input", fun test_process_invalid/0},
{"Process with timeout", fun test_process_timeout/0}
]}.
%%% Tests
test_process_valid() ->
Input = <<"valid input">>,
{ok, Result} = mylib:process(Input),
?assertMatch(#{data := _}, Result).
test_process_invalid() ->
Input = <<"">>,
?assertEqual({error, invalid_input}, mylib:process(Input)).
test_process_timeout() ->
Input = <<"data">>,
Options = #{timeout => 1},
?assertMatch({error, timeout}, mylib:process(Input, Options)).
%%% Property-Based Tests
prop_never_crashes_test_() ->
{timeout, 60, fun() ->
?assert(proper:quickcheck(prop_no_crash(), [{numtests, 1000}]))
end}.
prop_no_crash() ->
?FORALL(Input, binary(),
case mylib:process(Input) of
{ok, _} -> true;
{error, _} -> true
end
).
Common Test Suites
-module(mylib_SUITE).
-include_lib("common_test/include/ct.hrl").
%% CT callbacks
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1]).
-export([init_per_testcase/2, end_per_testcase/2]).
%% Test cases
-export([
test_basic_processing/1,
test_concurrent_processing/1,
test_error_handling/1
]).
%%% CT Callbacks
all() ->
[
{group, basic},
{group, concurrent},
{group, errors}
].
groups() ->
[
{basic, [parallel], [test_basic_processing]},
{concurrent, [parallel], [test_concurrent_processing]},
{errors, [sequence], [test_error_handling]}
].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(mylib),
Config.
end_per_suite(_Config) ->
application:stop(mylib),
ok.
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
ok.
%%% Test Cases
test_basic_processing(Config) ->
Input = <<"test data">>,
{ok, Result} = mylib:process(Input),
ct:log("Result: ~p", [Result]),
#{data := Data} = Result,
true = is_binary(Data).
test_concurrent_processing(Config) ->
Inputs = [<<"input", (integer_to_binary(N))/binary>> || N <- lists:seq(1, 100)],
Self = self(),
[spawn(fun() ->
{ok, _} = mylib:process(Input),
Self ! {done, N}
end) || {N, Input} <- lists:enumerate(Inputs)],
receive_n(100).
test_error_handling(Config) ->
{error, invalid_input} = mylib:process(<<>>),
{error, timeout} = mylib:process(<<"data">>, #{timeout => 1}).
%%% Helpers
receive_n(0) -> ok;
receive_n(N) ->
receive
{done, _} -> receive_n(N - 1)
after 5000 ->
ct:fail("Timeout waiting for concurrent operations")
end.
PropEr Property Tests
-module(mylib_prop).
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
%%% Generators
user_id() ->
non_neg_integer().
user_name() ->
?LET(Len, range(1, 50),
?LET(Chars, vector(Len, range($a, $z)),
list_to_binary(Chars))).
valid_user() ->
?LET({Id, Name, Email}, {user_id(), user_name(), user_name()},
#{id => Id, name => Name, email => <<Email/binary, "@example.com">>}).
%%% Properties
prop_create_user_returns_user() ->
?FORALL({Name, Email}, {user_name(), user_name()},
begin
{ok, User} = mylib:create_user(Name, Email),
maps:is_key(id, User) andalso
maps:get(name, User) =:= Name andalso
maps:get(email, User) =:= Email
end).
prop_roundtrip_serialization() ->
?FORALL(User, valid_user(),
begin
Serialized = mylib:serialize(User),
{ok, Deserialized} = mylib:deserialize(Serialized),
User =:= Deserialized
end).
%%% EUnit wrapper
properties_test_() ->
{timeout, 120, [
?_assert(proper:quickcheck(prop_create_user_returns_user(), [{numtests, 100}])),
?_assert(proper:quickcheck(prop_roundtrip_serialization(), [{numtests, 100}]))
]}.
Publishing to Hex
Hex Package Configuration
Add to rebar.config:
{project_plugins, [
rebar3_hex,
rebar3_ex_doc
]}.
{hex, [
{doc, #{provider => ex_doc}}
]}.
{ex_doc, [
{source_url, <<"https://github.com/username/mylib">>},
{extras, [<<"README.md">>, <<"LICENSE">>, <<"CHANGELOG.md">>]},
{main, <<"readme">>}
]}.
Pre-publish Checklist
- Version bumped in
.app.src - CHANGELOG.md updated with version changes
- README.md is current and comprehensive
- All tests pass:
rebar3 do eunit, ct - Dialyzer passes:
rebar3 dialyzer - Documentation builds:
rebar3 edoc - Application metadata complete in
.app.src - License file(s) included
- No uncommitted changes
- Dry run succeeds:
rebar3 hex publish --dry-run
Publishing Commands
# Generate documentation
rebar3 hex docs
# Dry run to check package
rebar3 hex publish --dry-run
# Actually publish
rebar3 hex publish
# Publish documentation separately
rebar3 hex publish docs
# Revert a package (within 24h or 1 hour for new packages)
rebar3 hex revert 0.1.0
# Retire a version (soft deprecation)
rebar3 hex retire mylib 0.1.0 other "Please upgrade to 0.2.0"
Version Retirement Reasons
# Available retirement reasons:
rebar3 hex retire mylib VERSION other "Custom message"
rebar3 hex retire mylib VERSION security "Security vulnerability found"
rebar3 hex retire mylib VERSION deprecated "Use new-package instead"
rebar3 hex retire mylib VERSION invalid "Invalid release"
rebar3 hex retire mylib VERSION renamed "Package renamed to new-name"
Application Supervision
Application Behavior
-module(mylib_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
case mylib_sup:start_link() of
{ok, Pid} ->
{ok, Pid};
Error ->
Error
end.
stop(_State) ->
ok.
Root Supervisor
-module(mylib_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
SupFlags = #{
strategy => one_for_one,
intensity => 5,
period => 60
},
ChildSpecs = [
#{
id => mylib_server,
start => {mylib_server, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [mylib_server]
},
#{
id => mylib_pool_sup,
start => {mylib_pool_sup, start_link, []},
restart => permanent,
shutdown => infinity,
type => supervisor,
modules => [mylib_pool_sup]
}
],
{ok, {SupFlags, ChildSpecs}}.
Worker Pool Supervisor
-module(mylib_pool_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
PoolSize = application:get_env(mylib, pool_size, 10),
SupFlags = #{
strategy => one_for_one,
intensity => 10,
period => 60
},
ChildSpecs = [
#{
id => {mylib_worker, N},
start => {mylib_worker, start_link, [N]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [mylib_worker]
}
|| N <- lists:seq(1, PoolSize)
],
{ok, {SupFlags, ChildSpecs}}.
Configuration Management
Application Environment
% src/mylib.app.src
{application, mylib,
[{env, [
{pool_size, 10},
{timeout, 5000},
{retry_count, 3},
{log_level, info}
]}
]}.
Runtime Configuration
-module(mylib_config).
-export([get/1, get/2, set/2]).
%% @doc Get configuration value
-spec get(Key :: atom()) -> term().
get(Key) ->
case application:get_env(mylib, Key) of
{ok, Value} -> Value;
undefined -> error({config_not_found, Key})
end.
%% @doc Get configuration value with default
-spec get(Key :: atom(), Default :: term()) -> term().
get(Key, Default) ->
application:get_env(mylib, Key, Default).
%% @doc Set configuration value at runtime
-spec set(Key :: atom(), Value :: term()) -> ok.
set(Key, Value) ->
application:set_env(mylib, Key, Value).
Config Files (sys.config)
% config/sys.config
[
{mylib, [
{pool_size, 20},
{timeout, 10000},
{log_level, debug}
]},
{sasl, [
{sasl_error_logger, {file, "log/sasl-error.log"}},
{errlog_type, error}
]},
{kernel, [
{logger_level, info}
]}
].
Common Patterns
Singleton Server
-module(mylib_server).
-behaviour(gen_server).
%% API
-export([start_link/0, call/1, cast/1, get_state/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-define(SERVER, ?MODULE).
%%% API
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
call(Request) ->
gen_server:call(?SERVER, Request).
cast(Request) ->
gen_server:cast(?SERVER, Request).
get_state() ->
gen_server:call(?SERVER, get_state).
%%% Callbacks
init([]) ->
State = #{
started_at => erlang:system_time(second),
requests => 0
},
{ok, State}.
handle_call(get_state, _From, State) ->
{reply, State, State};
handle_call(Request, _From, State = #{requests := Count}) ->
Result = process_request(Request),
{reply, Result, State#{requests => Count + 1}}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
%%% Internal
process_request(Request) ->
{ok, Request}.
Resource Pool Pattern
-module(mylib_pool).
-export([checkout/0, checkin/1, with_resource/1]).
-define(POOL, mylib_resource_pool).
%% @doc Check out a resource from the pool
-spec checkout() -> {ok, pid()} | {error, no_resources}.
checkout() ->
case ets:lookup(?POOL, available) of
[{available, [Pid | Rest]}] ->
ets:insert(?POOL, {available, Rest}),
ets:insert(?POOL, {in_use, Pid}),
{ok, Pid};
_ ->
{error, no_resources}
end.
%% @doc Return a resource to the pool
-spec checkin(pid()) -> ok.
checkin(Pid) ->
ets:delete_object(?POOL, {in_use, Pid}),
[{available, Available}] = ets:lookup(?POOL, available),
ets:insert(?POOL, {available, [Pid | Available]}),
ok.
%% @doc Execute function with a resource
-spec with_resource(fun((pid()) -> term())) -> {ok, term()} | {error, term()}.
with_resource(Fun) ->
case checkout() of
{ok, Resource} ->
try
Result = Fun(Resource),
{ok, Result}
after
checkin(Resource)
end;
Error ->
Error
end.
Anti-Patterns
1. Exposing Internal Processes
% Bad: Exposes internal PIDs
-spec get_worker() -> pid().
get_worker() ->
whereis(mylib_worker).
% Good: Provide functional API
-spec process(Input :: term()) -> {ok, term()} | {error, term()}.
process(Input) ->
mylib_worker:process(Input).
2. Breaking Module Contracts
% Bad: Changing return type in new version
% v0.1.0
-spec parse(binary()) -> map().
% v0.2.0 - BREAKING
-spec parse(binary()) -> {ok, map()} | {error, term()}.
% Good: Add new function, deprecate old
% v0.2.0
-spec parse(binary()) -> map(). % Deprecated
-spec parse_safe(binary()) -> {ok, map()} | {error, term()}.
3. Ignoring Application Lifecycle
% Bad: Starting processes in module
-module(mylib).
start_worker() ->
spawn(fun() -> worker_loop() end).
% Good: Use supervision tree
-module(mylib_sup).
init([]) ->
ChildSpecs = [worker_spec()],
{ok, {sup_flags(), ChildSpecs}}.
4. Missing Type Specs
% Bad: No specs, dialyzer can't verify
process(Input) ->
transform(Input).
% Good: Full specifications
-spec process(Input :: binary()) -> {ok, term()} | {error, atom()}.
process(Input) when is_binary(Input) ->
transform(Input).
References
meta-library-dev- Foundational library patternslang-erlang-dev- Core Erlang/OTP patterns- Rebar3 Documentation
- Hex Package Manager
- EDoc User's Guide
- Dialyzer User's Guide
- OTP Design Principles