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
or optional
types is not trivial and requires a lot of boilerplate.
std::optional
doesn't support visitation.
The interface of std::variant
and std::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
and optional
implementations, providing a single implementation-independent interface.
Provides "pattern matching"-like syntax for visitation and recursive visitation which works both for variant
and optional
.
Provides an intuitive placeholder-based recursive variant
and optional
type definition.
Provides monadic operations such as map
and and_then
for optional
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 arity N + 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
, where N
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);
optional
operationsscelta
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's CMakeLists.txt
and look for the scelta
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)
scelta::nonrecursive::visit
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
);
scelta::nonrecursive::match
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 of functionObjects...
and returns a new function which takes a number of visitables...
.
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 all functionObjects...
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
);
scelta::recursive::builder
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 more placeholder
alternatives. (i.e. both optional and variant)
placeholder
is replaced with the recursive ADT itself when using type
or resolve
.
type
returns a wrapper around a fully-resolved recursive ADT
.
resolve
returns a fully-resolved alternative contained in ADT
.
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};
scelta::recursive::visit
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 not noexcept
-friendly.
The operator()
overloads of visitor...
must take one extra generic argument to receive the recurse
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};
scelta::recursive::match
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 not noexcept
-friendly.
The passed functionObjects...
must take one extra generic argument to receive the recurse
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);
scelta::match
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);
optional
operationsscelta
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)
f(*o)
if o
is set, f_def()
otherwise.map_or(o, def, f)
f(*o)
if o
is set, def
otherwise.map(o, f)
optional{f(*o)}
if o
is set, an empty optional otherwise.and_then(o, f)
f(*o)
if o
is set, an empty optional otherwise.and_(o, ob)
ob
if o
is set, an empty ob
otherwise.or_else(o, f)
o
if o
is set, f()
otherwise.or_(o, def)
o
if o
is set, def
otherwise.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"