lang-erlang-library-dev
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.
When & Why to Use This Skill
This Claude skill provides expert-level patterns and best practices for Erlang/OTP library development. It streamlines the creation of robust, reusable Erlang components by offering specialized guidance on rebar3 configuration, OTP-compliant application structures, and public API design using process-oriented patterns. It also integrates essential ecosystem tools like Hex for package management, Dialyzer for type safety, and EDoc for documentation, ensuring your libraries meet professional Erlang standards and idiomatic practices.
Use Cases
- Bootstrapping a new Erlang library or OTP application using rebar3 with a standardized project structure and optimized configuration.
- Designing idiomatic public APIs and custom behaviors to ensure library extensibility, process safety, and clean module organization.
- Implementing comprehensive testing strategies and quality assurance using EUnit, Common Test, and property-based testing with PropEr.
- Managing package metadata, versioning, and documentation for publishing high-quality Erlang libraries to the Hex package manager.
- Configuring Dialyzer and type specifications to enforce static analysis and improve code reliability in complex Erlang systems.
| 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