Erlando brings lightweight functional-programming extensions to Erlang. It
provides cut expressions, Haskell-style do notation, aliased imports, a
typeclass runtime, common typeclasses, monads, and monad transformers.
- Erlang/OTP 21 or later
- rebar3
The project CI configuration currently covers OTP 21 through OTP 29.
Add Erlando as a dependency:
{deps, [
{erlando, {git, "https://github.com/slepher/erlando.git", {tag, "2.11.1"}}}
]}.Projects that declare their own typeclasses or instances also need the
rebar3_erlando compiler plugin:
{plugins, [
{rebar3_erlando,
{git, "https://github.com/slepher/rebar3_erlando.git", {tag, "0.3.0"}}}
]}.
{provider_hooks, [
{post, [{compile, {erlando, compile}}]}
]}.The hook collects typeclass and instance metadata after compilation and
generates the typeclass dispatch module. Applications that only use the
instances shipped by Erlando do not need to add this hook.
Include cut.hrl and use _ in an expression to create a function. Each hole
becomes an argument, from left to right:
-module(cut_example).
-include("cut.hrl").
add(A, B) -> A + B.
example() ->
Add10 = add(10, _),
15 = Add10(5),
Pair = {_, _},
{left, right} = Pair(left, right).Abstraction happens at the shallowest enclosing expression. For example:
list_to_binary([1, math:pow(2, _)])is equivalent to:
list_to_binary([1, fun(X) -> math:pow(2, X) end])Cuts work in calls, operators, tuples, lists, maps, records, binaries, case
expressions, and comprehensions. Because a cut produces a normal fun, its
arguments follow Erlang's usual eager evaluation rules.
Include do.hrl to enable do/1. Erlando uses list-comprehension syntax for
monadic binding:
-module(do_example).
-include("do.hrl").
safe_square(Value) ->
do([monad_maybe ||
true <- return(is_number(Value)),
return(Value * Value)]).A more typical error-handling example is:
read_file(Path) ->
do([error_m ||
Handle <- file:open(Path, [read, binary]),
Data <- file:read(Handle, 4096),
file:close(Handle),
return(Data)]).Inside a do block:
Pattern <- Expressionbinds through the selected monad.- Ordinary expressions are sequenced through the monad.
- Calls to
return(...)andfail(...)are directed to the selected monad. Pattern = Expressionis a normal Erlang match.
The expression:
do([Monad || A <- First, Next(A)])is transformed conceptually into:
monad:'>>='(First, fun(A) -> Next(A) end, Monad)The import_as parse transform imports a remote function under a local name:
-module(import_example).
-compile({parse_transform, import_as}).
-import_as({lists, [{duplicate/2, dup}, {reverse/1, rev}]}).
example() ->
[a, a, a] = dup(3, a),
[3, 2, 1] = rev([1, 2, 3]).The alias is implemented as a local function, so expressions such as
fun dup/2 also work.
A typeclass is an Erlang behaviour marked with -superclass/1:
-module(functor).
-superclass([]).
-callback fmap(fun((A) -> B), f(F, A), F) -> f(F, B).Superclass relationships are declared by module name:
-module(monad).
-superclass([applicative]).Erlando includes these typeclasses:
functor,applicative,monadfoldable,traversablealternative,monad_plusmonad_reader,monad_writer,monad_state,monad_contmonad_error,monad_fail,monad_trans,monad_runnermonoid
The full typeclass entry points accept a type descriptor as their last argument. Generated convenience forms can infer it from a registered runtime value. A plain atom selects a concrete instance; a tuple can carry an inner typeclass for a transformer:
{identity, 2} = functor:fmap(fun(X) -> X + 1 end, {identity, 1}),
StateT = {state_t, identity},
StateValue = monad:return(ok, StateT).-erlando_instance(...) is the single source of truth for an instance. It
registers the represented type and its capabilities, adds the required Erlang
behaviours, generates requested callback adapters, and emits versioned BEAM
metadata for rebar3_erlando.
Every module that declares an instance must include the macro header:
-include("erlando_instance.hrl").Use type for the represented type, adapters for generated callback
adapters, and manual for capabilities whose callbacks are implemented by the
module itself:
-module(identity).
-include("erlando_instance.hrl").
-erlando_instance(#{
type => {identity, [identity/1]},
adapters => [#{
mode => target,
patterns => [identity],
capabilities => [functor, applicative, monad, monad_fail]
}],
manual => [monad_runner]
}).
-export_type([identity/1]).
-type identity(A) :: {identity, A}.mode => target generates callbacks whose type descriptor is matched at the
end of the call. patterns lists the descriptor patterns accepted by the
adapter. This is the usual mode for a concrete, non-parameterized type.
The type name does not have to match the module name:
-module(function_instance).
-include("erlando_instance.hrl").
-erlando_instance(#{
type => {function, [function_instance/0]},
adapters => [#{
mode => target,
patterns => [function],
capabilities => [functor, applicative, monad, monad_reader]
}],
manual => [monad_runner]
}).Here the typeclass registry maps the type function to the implementation
module function_instance.
Use a source adapter when the generated callback must forward an inner typeclass descriptor to the implementation:
-module(state_t).
-include("erlando_instance.hrl").
-erlando_instance(#{
type => {state_t, [state_t/3]},
adapters => [
#{mode => source,
requires => functor,
capabilities => [functor]},
#{mode => source,
requires => monad,
capabilities => [applicative, monad, monad_trans, monad_state]}
],
manual => [monad_runner]
}).mode => source forwards the inner descriptor required by requires. A
source adapter must specify either requires or explicit args. Adapter
groups may also use the lower-level remote, patterns_group, extra_call,
and am options supported by the callback generator.
An implementation module can provide one capability for several types:
-module(monad_reader_instance).
-include("erlando_instance.hrl").
-erlando_instance(#{
types => [state_t, cont_t, maybe_t, error_t, except_t, list_t],
capability => monad_reader,
implementation => generic
}).This registers monad_reader_instance as the monad_reader implementation
for every listed type. The module supplies the generic callbacks itself; no
adapter functions are generated.
For example:
monad_reader:ask(state_t)is dispatched to:
monad_reader_instance:ask(state_t)When different types need different local callback implementations, use a
dispatch map. Every callback entry is {LocalFunction, Arity}:
-erlando_instance(#{
types => [reader_t, state_t],
capability => monad_cont,
implementation => {dispatch, #{
reader_t => #{callCC => {reader_t_call_cc, 2}},
state_t => #{callCC => {state_t_call_cc, 2}}
}}
}).The macro exports the public typeclass callbacks and routes both atom and tuple descriptors to the selected local function.
Top-level keys:
type: one type name, or{TypeName, [ExportedType/Arity, ...]}.types: a non-empty list of type declarations for a shared implementation.adapters: adapter groups that generate callbacks.manual: capabilities implemented directly in the declaring module.capability: one capability implemented for every declared type.implementation:generic, or{dispatch, DispatchMap}when used withcapability.
Adapter-group keys:
mode:targetorsource.capabilities: typeclasses sharing this adapter configuration.patterns: descriptor patterns used by a target adapter.requires: inner typeclass required by a source adapter.remote: module that owns the underlying callback implementation.
A declaration must contain exactly one logical entry for each capability.
Duplicate types or capabilities are rejected during macro expansion, and a
source adapter without requires or args is invalid.
The macro stores normalized, versioned erlando_instance_meta in the compiled
BEAM. After Erlando's compiler hook runs, the generated typeclass module
provides:
typeclass:is_typeclass/1to identify registered typeclasses.typeclass:module/2to resolve{Type, Typeclass}to its implementation.typeclass:type/1to infer a registered type from a runtime value.
Metadata is compile-time input. Do not edit the generated typeclass module by
hand; change the -superclass(...) or -erlando_instance(...) declaration and
compile again.
The repository includes concrete instances such as identity, either,
monad_maybe, error_m, reader_m, writer_m, state_m, cont_m, lists,
functions, and tuples. It also includes reader_t, writer_t, state_t,
cont_t, maybe_t, error_t, except_t, and list_t transformers.
Transformer descriptors use {Transformer, InnerTypeclass}. For example:
StateT = state_t:new(identity),
Computation = do([StateT ||
monad_state:put(initial_state),
Value <- monad_state:get(),
return(Value)]),
identity:run(state_t:eval(Computation, undefined, StateT)).Compile and run the test suite with:
rebar3 compile
rebar3 ctOther configured checks include:
rebar3 xref
rebar3 dialyzerErlando source files are distributed under the Mozilla Public License 1.1; see the license header in each source file.