C++17 zero-overhead syntactic sugar for
variant
andoptional
.
std::variant
and std::optional
were introduced to C++17's Standard Library. They are sum types that can greatly improve type safety and performance.
However, there are some problems with them:
-
The syntax of some common operations such as visitation is not as nice as it could be, and requires a significant amount of boilerplate.
-
Defining and using recursive
variant
oroptional
types is not trivial and requires a lot of boilerplate. -
std::optional
doesn't support visitation. -
The interface of
std::variant
andstd::optional
is different from some other commonly used ADT implementations - interoperability requires significant boilerplate.
scelta
aims to fix all the aformenetioned problems by providing zero-overhead syntactic sugar that:
-
Automatically detects and homogenizes all available
variant
andoptional
implementations, providing a single implementation-independent interface. -
Provides "pattern matching"-like syntax for visitation and recursive visitation which works both for
variant
andoptional
. -
Provides an intuitive placeholder-based recursive
variant
andoptional
type definition. -
Provides monadic operations such as
map
andand_then
foroptional
types, including infix syntax.
scelta
detects and works out-of-the-box with:
std::variant
boost::variant
mpark::variant
eggs::variant
type_safe::variant
std::optional
boost::optional
type_safe::optional
tl::optional
Other implementation can be easily adapted by providing specializations of the helper traits
structs. PRs are welcome!
scelta
provides curried, constexpr
-friendly, and SFINAE-friendly visitation utilities both for variant
and optional
. The final user syntax resembles pattern matching. Recursive data structures are supported.
using shape = std::variant<circle, box>;
shape s0{circle{/*...*/}};
shape s1{box{/*...*/}};
// In place `match` visitation.
scelta::match([](circle, circle){ /* ... */ },
[](circle, box) { /* ... */ },
[](box, circle){ /* ... */ },
[](box, box) { /* ... */ })(s0, s1);
The match
function is intentionally curried in order to allow reuse of a particular visitor in a scope, even on different implementations of variant
/optional
.
using boost_optstr = boost::optional<std::string>;
using std_optstr = std::optional<std::string>;
// Curried `match` usage.
auto print = scelta::match([](std::string s) { cout << s; },
[](scelta::nullopt_t){ cout << "empty"; });
boost_optstr s0{/*...*/};
std_optstr s1{/*...*/};
// Implementation-independent visitation.
print(s0);
print(s1);
Recursive variant
and optional
data structures can be easily created through the use of placeholders.
namespace impl
{
namespace sr = scelta::recursive;
// `placeholder` and `builder` can be used to define recursive
// sum types.
using _ = sr::placeholder;
using builder = sr::builder<std::variant<int, std::vector<_>>>;
// `type` evaluates to the final recursive data structure type.
using type = sr::type<builder>;
// `resolve` completely evaluates one of the alternatives.
// (In this case, even the `Allocator` template parameter is
// resolved!)
using vector_type = sr::resolve<builder, std::vector<_>>;
}
using int_tree = impl::type;
using int_tree_vector = impl::vector_type;
After defining recursive structures, in place recursive visitation is also possible. scelta
provides two ways of performing recursive visitation:
-
scelta::match(/* base cases */)(/* recursive cases */)(/* visitables */)
This is an "homogeneous"
match
function that works for both non-recursive and recursive visitation. The first invocation always takes an arbitrary amount of base cases. If recursive cases are provided to the second invocation, then a third invocation with visitables is expected. Unless explicitly provided, the return type is deduced from the base cases.The base cases must have arity
N
, the recursive cases must have arityN + 1
.N
is the number of visitables that will be provided. -
scelta::recursive::match</* return type */>(/* recursive cases */)(/* visitables */)
This version always requires an explicit return type and an arbitrary amount of recursive cases with arity
N + 1
, whereN
is the number of visitables that will be provided.
int_tree t0{/*...*/};
scelta::match(
// Base case.
[](int x){ cout << x; }
)(
// Recursive case.
[](auto recurse, int_tree_vector v){ for(auto x : v) recurse(v); }
)(t0);
// ... or ...
scelta::recursive::match<return_type>(
// Base case.
[](auto, int x){ cout << x; },
// Recursive case.
[](auto recurse, int_tree_vector v){ for(auto x : v) recurse(v); }
)(t0);
scelta
provides various monadic operations that work on any supported optional
type. Here's an example inspired by Simon Brand's "Functional exceptionless error-handling with optional and expected" article:
optional<image_view> crop_to_cat(image_view);
optional<image_view> add_bow_tie(image_view);
optional<image_view> make_eyes_sparkle(image_view);
image_view make_smaller(image_view);
image_view add_rainbow(image_view);
optional<image_view> get_cute_cat(image_view img)
{
using namespace scelta::infix;
return crop_to_cat(img)
| and_then(add_bow_tie)
| and_then(make_eyes_sparkle)
| map(make_smaller)
| map(add_rainbow);
}
scelta
is an header-only library. It is sufficient to include it.
// main.cpp
#include <scelta.hpp>
int main() { return 0; }
g++ -std=c++1z main.cpp -Isome_path/scelta/include
Tests can be easily built and run using CMake.
git clone https://github.com/SuperV1234/scelta && cd scelta
./init-repository.sh # get `vrm_cmake` dependency
mkdir build && cd build
cmake ..
make check # build and run tests
make example_error_handling # error handling via pattern matching
make example_expression # recursive expression evaluation
make example_optional_cat # monadic optional operations
All tests currently pass on Arch Linux x64
with:
-
g++ (GCC) 8.0.0 20170514 (experimental)
-
clang version 5.0.0 (trunk 303617)
-
Add this repository and SuperV1234/vrm_cmake as submodules of your project, in subfolders inside
your_project/extlibs/
:git submodule add https://github.com/SuperV1234/vrm_cmake.git your_project/extlibs/vrm_cmake git submodule add https://github.com/SuperV1234/scelta.git your_project/extlibs/scelta
-
Include
vrm_cmake
in your project'sCMakeLists.txt
and look for thescelta
extlib:# Include `vrm_cmake`: list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/extlibs/vrm_cmake/cmake/") include(vrm_cmake) # Find `scelta`: vrm_cmake_find_extlib(scelta)
Executes non-recursive visitation.
-
Interface:
template <typename Visitor, typename... Visitables> constexpr /*deduced*/ visit(Visitor&& visitor, Visitables&&... visitables) noexcept(/*deduced*/);
-
visitables...
must all be the same type. (i.e. different implementations of variant/optional currently cannot be mixed together) -
visitor
must be invocable with all the alternatives of the passed visitables.
-
-
Examples:
struct visitor { auto operator()(int) { return 0; } auto operator()(char){ return 1; } }; variant<int, char> v0{'a'}; assert( scelta::nonrecursive::visit(visitor{}, v0) == 1 );
struct visitor { auto operator()(int) { return 0; } auto operator()(scelta::nullopt_t){ return 1; } }; optional<int> o0{0}; assert( scelta::nonrecursive::visit(visitor{}, o0) == 0 );
Executes non-recursive in-place visitation.
-
Interface:
template <typename... FunctionObjects> constexpr /*deduced*/ match(FunctionObjects&&... functionObjects) noexcept(/*deduced*/) { return [o = overload(functionObjects...)](auto&&... visitables) noexcept(/*deduced*/) -> /*deduced*/ { // ... perform visitation with `scelta::nonrecursive::visit` ... }; };
-
Invoking
match
takes a number offunctionObjects...
and returns a new function which takes a number ofvisitables...
. -
visitables...
must all be the same type. (i.e. different implementations of variant/optional currently cannot be mixed together) -
o
must be invocable with all the alternatives of the passed visitables. (i.e. the overload of allfunctionObjects...
must produce an exhaustive visitor)
-
-
Examples:
variant<int, char> v0{'a'}; assert( scelta::nonrecursive::match([](int) { return 0; } [](char){ return 1; })(v0) == 1 );
optional<int> o0{0}; assert( scelta::nonrecursive::match([](int) { return 0; } [](scelta::nullopt_t){ return 1; })(o0) == 1 );
Allows placeholder-based definition of recursive ADTs.
-
Interface:
template <typename ADT> class builder; struct placeholder; template <typename Builder> using type = /* ... recursive ADT type wrapper ... */; template <typename Builder, typename T> using resolve = /* ... resolved ADT alternative ... */;
-
builder
takes any ADT containing zero or moreplaceholder
alternatives. (i.e. both optional and variant) -
placeholder
is replaced with the recursive ADT itself when usingtype
orresolve
. -
type
returns a wrapper around a fully-resolved recursiveADT
. -
resolve
returns a fully-resolved alternative contained inADT
.
-
-
Examples:
using _ = scelta::recursive::placeholder; using b = scelta::recursive::builder<variant<int, _*>>; using recursive_adt = scelta::recursive::type<b>; using ptr_alternative = scelta::recursive::resolve<b, _*>; recursive_adt v0{0}; recursive_adt v1{&v0};
Executes recursive visitation.
-
Interface:
template <typename Return, typename Visitor, typename... Visitables> constexpr Return visit(Visitor&& visitor, Visitables&&... visitables) noexcept(false);
-
Similar to
scelta::nonrecursive::visit
, but requires an explicit return type and is notnoexcept
-friendly. -
The
operator()
overloads ofvisitor...
must take one extra generic argument to receive therecurse
helper.
-
-
Examples:
using _ = scelta::recursive::placeholder; using b = scelta::recursive::builder<variant<int, std::vector<_>>>; using recursive_adt = scelta::recursive::type<b>; using rvec = scelta::recursive::resolve<b, std::vector<_>>; struct visitor { auto operator()(auto, int x) { /* base case */ }, auto operator()(auto recurse, rvec& v){ for(auto& x : v) recurse(x); } }; recursive_adt v0{rvec{recursive_adt{0}, recursive_adt{1}}}; scelta::recursive::visit(visitor{}, v0};
Executes recursive visitation.
-
Interface:
template <typename Return, typename... FunctionObjects> constexpr auto match(FunctionObjects&&... functionObjects) noexcept(false) { return [o = overload(functionObjects...)](auto&&... visitables) noexcept(false) -> Return { // ... perform visitation with `scelta::recursive::visit` ... }; };
-
Similar to
scelta::nonrecursive::match
, but requires an explicit return type and is notnoexcept
-friendly. -
The passed
functionObjects...
must take one extra generic argument to receive therecurse
helper.
-
-
Examples:
using _ = scelta::recursive::placeholder; using b = scelta::recursive::builder<variant<int, std::vector<_>>>; using recursive_adt = scelta::recursive::type<b>; using rvec = scelta::recursive::resolve<b, std::vector<_>>; recursive_adt v0{rvec{recursive_adt{0}, recursive_adt{1}}}; scelta::recursive::match( [](auto, int x) { /* base case */ }, [](auto recurse, rvec& v){ for(auto& x : v) recurse(x); } )(v0);
Executes visitation (both non-recursive and recursive). Attempts to deduce the return type from the base cases, optionally supports user-provided explicit return type.
-
Interface:
template <typename Return = impl::deduce_t, typename... BaseCases> constexpr auto match(BaseCases&&... baseCases) { return [bco = overload(adapt(baseCases)...)](auto... xs) { if constexpr(are_visitables<decltype(xs)...>()) { // ... perform visitation with `scelta::nonrecursive::visit` ... } else { return [o = overload(bco, xs...)](auto&&... visitables) { // ... perform visitation with `scelta::recursive::visit` ... }; } }; };
-
The first invocation of
scelta::match
takes one or more base cases. A base case is a function object with the same arity as the number of objects that will be visited. -
The function returned by the first invocation takes either a number of recursive cases or a number of visitables.
-
Recursive cases are function objects with arity equal to the number of objects that will be visited plus one (the +1 is for the
recurse
argument). -
Visitables are variants or optionals. If visitables are passed here, non-recursive visitation will be performed immediately.
-
-
If recursive cases were passed, the last returned function takes any number of visitables. Recursive visitation will then be performed immediately.
-
-
Examples:
variant<int, char> v0{'a'}; assert( scelta::match([](int) { return 0; } [](char){ return 1; })(v0) == 1 );
using _ = scelta::recursive::placeholder; using b = scelta::recursive::builder<variant<int, std::vector<_>>>; using recursive_adt = scelta::recursive::type<b>; using rvec = scelta::recursive::resolve<b, std::vector<_>>; recursive_adt v0{rvec{recursive_adt{0}, recursive_adt{1}}}; scelta::match( [](int x){ /* base case */ } )( [](auto recurse, rvec& v){ for(auto& x : v) recurse(x); } )(v0);
scelta
provides various monadic optional
operations. They can be used in two different ways:
optional<int> o{/* ... */};
// Free function syntax:
scelta::map(o, [](int x){ return x + 1; });
// Infix syntax:
o | scelta::infix::map([](int x){ return x + 1; });
These are the available operations:
-
map_or_else(o, f_def, f)
- Returns
f(*o)
ifo
is set,f_def()
otherwise.
- Returns
-
map_or(o, def, f)
- Returns
f(*o)
ifo
is set,def
otherwise.
- Returns
-
map(o, f)
- Returns
optional{f(*o)}
ifo
is set, an empty optional otherwise.
- Returns
-
and_then(o, f)
- Returns
f(*o)
ifo
is set, an empty optional otherwise.
- Returns
-
and_(o, ob)
- Returns
ob
ifo
is set, an emptyob
otherwise.
- Returns
-
or_else(o, f)
- Returns
o
ifo
is set,f()
otherwise.
- Returns
-
or_(o, def)
- Returns
o
ifo
is set,def
otherwise.
- Returns
The example file example/optional_cat.cpp
shows usage of map
and and_then
using scelta::infix
syntax.
-
ACCU 2017 talk: "Implementing variant Visitation Using Lambdas"
-
C++Now 2017 talk: "Implementing
variant
visitation using lambdas" -
C++::London (May 2017) talk: "Implementing
variant
visitation using lambdas"