scelta

by SuperV1234

SuperV1234 / scelta

(experimental) Syntactic sugar for variant and optional types.

130 Stars 7 Forks Last release: Not found MIT License 122 Commits 2 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

scelta

C++17 zero-overhead syntactic sugar for

variant
and
optional
.

build stability license gratipay conan badge.cpp on-wandbox on-godbolt

Table of Contents

Overview

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.

Implementation independent

scelta
detects and works out-of-the-box with:

Other implementation can be easily adapted by providing specializations of the helper

traits
structs. PRs are welcome!

Curried visitation syntax

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;

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<:string>;
using std_optstr = std::optional<:string>;

// Curried match usage. auto print = scelta::match([](std::string s) { cout << s; }, { cout << "empty"; });

boost_optstr s0{/.../}; std_optstr s1{/.../};

// Implementation-independent visitation. print(s0); print(s1); </:string></:string>

Recursive ADTs creation and visitation

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<:variant std::vector>&gt;&gt;;

// `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>&gt;;

}

using int_tree = impl::type; using int_tree_vector = impl::vector_type; </:variant>

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( // Base case. [](auto, int x){ cout << x; },

// Recursive case.
[](auto recurse, int_tree_vector v){ for(auto x : v) recurse(v); }

)(t0);

Monadic
optional
operations

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 crop_to_cat(image_view);
optional add_bow_tie(image_view);
optional make_eyes_sparkle(image_view);
image_view make_smaller(image_view);
image_view add_rainbow(image_view);

optional 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); }

Installation/usage

Quick start

scelta
is an header-only library. It is sufficient to include it.

// main.cpp
#include 

int main() { return 0; } </scelta.hpp>

g++ -std=c++1z main.cpp -Isome_path/scelta/include

Running tests and examples

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)
    ~~

Integration with existing project

  1. 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
    
  2. 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)

Documentation

scelta::nonrecursive::visit

Executes non-recursive visitation.

  • Interface:

    template 
    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 v0{'a'}; assert( scelta::nonrecursive::visit(visitor{}, v0) == 1 );

    struct visitor
    {
        auto operator()(int)              { return 0; }
        auto operator()(scelta::nullopt_t){ return 1; }
    };
    
    optional o0{0};
    assert(
        scelta::nonrecursive::visit(visitor{}, o0) == 0
    );
    

scelta::nonrecursive::match

Executes non-recursive in-place visitation.

  • Interface:

    template 
    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 v0{'a'};
    assert(
        scelta::nonrecursive::match([](int) { return 0; }
                                    [](char){ return 1; })(v0) == 1
    );
    
    optional 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 
    class builder;
    
    

    struct placeholder;

    template using type = /* ... recursive ADT type wrapper ... */;

    template 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>;
    
    

    using recursive_adt = scelta::recursive::type; using ptr_alternative = scelta::recursive::resolve;

    recursive_adt v0{0}; recursive_adt v1{&v0};

scelta::recursive::visit

Executes recursive visitation.

  • Interface:

    template 
    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>>;
    
    

    using recursive_adt = scelta::recursive::type; using rvec = scelta::recursive::resolve>;

    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 
    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>>;
    
    

    using recursive_adt = scelta::recursive::type; using rvec = scelta::recursive::resolve>;

    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 
    constexpr auto match(BaseCases&&... baseCases)
    {
        return [bco = overload(adapt(baseCases)...)](auto... xs)
        {
            if constexpr(are_visitables())
            {
                // ... 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 v0{'a'};
    assert(
        scelta::match([](int) { return 0; }
                      [](char){ return 1; })(v0) == 1
    );
    
    using _ = scelta::recursive::placeholder;
    using b = scelta::recursive::builder>>;
    
    

    using recursive_adt = scelta::recursive::type; using rvec = scelta::recursive::resolve>;

    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);

Monadic
optional
operations

scelta
provides various monadic
optional
operations. They can be used in two different ways:
optional 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)
      if
      o
      is set,
      f_def()
      otherwise.
  • map_or(o, def, f)
    • Returns
      f(*o)
      if
      o
      is set,
      def
      otherwise.
  • map(o, f)
    • Returns
      optional{f(*o)}
      if
      o
      is set, an empty optional otherwise.
  • and_then(o, f)
    • Returns
      f(*o)
      if
      o
      is set, an empty optional otherwise.
  • and_(o, ob)
    • Returns
      ob
      if
      o
      is set, an empty
      ob
      otherwise.
  • or_else(o, f)
    • Returns
      o
      if
      o
      is set,
      f()
      otherwise.
  • or_(o, def)
    • Returns
      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.

Resources

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.