Skip to content

This a header only library which enables the library user to chain function calls in a readable fashion, that looks like piping commands in a shell script. While handling of errors is done implicitly.

License

Notifications You must be signed in to change notification settings

ferkulat/CppResultType

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

master: Build Status Build status

CppResultType

Table of contents

Motivation

In a lot of applications the biggest part is busyness logic that is not performance critical. But the implementer needs to deal with untrusted input, that needs to be checked. Or there are cases when returning a non valid value is a recoverable error or even not an error.

  • Checking for errors or invalid values lets the implementer write a lot of boiler plate code, which lacks readability quickly.
  • All to often: Checking results from function calls for errors is neglected and the "happy path" is the only path.
  • Throwing exceptions is a side effect and the user of the library will only know by peeking into the implementation or has to rely on documentation. The compiler will not be able to help you catching bugs. This is a solid basis for hard to find bugs.

Introduction

This a header only library which enables the library user to

  • chain function calls in a readable fashion, that looks like piping commands in a shell script.
  • Errors can be a visible part of the API by using/writing functions returning values which are able to hold either success or error.
  • the compiler will be able help you catch some bugs
  • checking for errors and leaving the call chain early with an error as result, is done implicitly by the library
  • the general idea is to return errors to the caller, which needs to deal with it.
  • this makes the functions easier to test

In general this only works if all functions take one parameter. To break such chains in case of an error, functions can return values of type Result<SuccessType, ErrorType>. This type holds either a value of type SuccessType or of type ErrorType. The function on the right side of the pipe will only called if the returned type on the left side holds a SucccessType. In case it holds an ErrorType the call chain will be left early.

The same holds for dealing with std::optional<T>, if the result on the left side holds a value, the function on the right side will be called. Other wise the chain returns early with std::nullopt.

Scott Wlaschin gave a talk about this way of programming, where he calls it "Railway oriented programming"

This implementation follows Scotts recommendation to have only one ErrorType to make it easier. Since an Error can be of type std::variant<ErrorType1, ..., ErrorTypeN> this not really a limitation.

Before C++17

A common way to return values and errors from functions is using output parameters or use some special values to indicate an error.

Here are both cases shown in one snippet:

int someFunction(int index)
{
    int value;

    // some times a lot of other code here...

    if (!someOtherFunction(index, &value)){
        return -1;
    }
}

The user of someFunction needs to know this, by reading either documentation or having a look into its implementation, that -1 indicates an error.

For someOtherFunction its more easy to see value as the desired result and the if-statement as a way to check if value is valid. But its breaks the flow of reading when you see int value; and (if you are lucky) in the next line the pointer to value in the parameter list. And its not possible to create a const int value.

Since C++17 (std::optional <T> )

C++17 introduced std::optional<T>. Which holds a boolean value to indicate the existence or absence of T. With this we can improve it:

std::optional<int> someFunction(int index)
{
    auto const opt_result1 = someOtherFunction(index);
    if(!opt_result1.has_value())
        return std::nullopt;
    }
    return opt_result1.value();
}

Function parameters are always input and we can communicate the absence of a value more clearly.

Explicit error checking

If we have to do more then one function call to create the result, it gets noisy:

std::optional<int> someFunction(SomeClass someClass)
{
    auto const opt_result1 = someOtherFunction(someClass);
    if(!opt_result1.has_value())
        return std::nullopt;
    }

    auto const opt_result2 = someReallyOtherFunction(opt_result1.value());
    if(!opt_result2.has_value())
        return std::nullopt;
    }
    return opt_result2.value();
}

Implicit error checking

But the approach of error checking and early return is the same in both cases. Lets put this in a special function by overloading operator|().

It enables us:

  • make sure that nothing on the right side of | will be called when on its left side an optional without a value in there appears.
  • make sure the result of this chain is a simple optional and not lots of nested optionals.
  • chain of an optional value with a function, which returns a non optional, returns an optional
template<typename Value, typename Function>
auto operator|(Value value, Function function){
        return function(value);
}

// lots of details are missing here
// its about the basic idea, not a working implementation

template<typename Value, typename Function>
auto operator|(std::optional<Value> value, Function function){
        if (value.has_value()){
            return function(value.value());
        }
        return std::nullopt
}

std::optional<int> someFunction(SomeClass someClass)
{
    return someClass | someOtherFunction | someReallyOtherFunction;
}

Overloading operator|() has one limitation: at least one of its parameters needs to be a non primitive type. And the function on the right side are pointers to functions. Which is already a primitive type. We could use lambdas to solve this.

Return other error values than true/false

The idea of std::optional<T> made more general, would be something like Result<Success, Error> where you are free to transport other error types than bool. It seems to make things easier to use the same type of Error in you whole project. Which is not really a limitation, since you can use a scoped enum or something like std::variant<ErrorType1, ErrorType2, ...>.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Err1, Err2, Err2};

struct Integer{value int}; // to make the overloaded operator|() work with normal functions

auto someComplexCalculation(Integer value)-> Integer{
    ...
}

auto someSimpleCalculation(Integer value)->Integer{
    ...
}
auto getInputFromUser()->std::string{
    ....
    return something;
}

auto convertToInt(std::string const& input) -> ResultType<Integer, Error>{
    std::stringstream s;
    int result;
    s << input;
    s >> result;
    if (s.fail()){
        return Error::Err1;
    }
    return Integer{result};
}

auto const result = getInputFromUser() | convertToInt | someComplexCalculation | someSimpleCalculation;

// to check for error
if (ResultType::IsError(result)){
    doSomethingWithTheError(result.CRefError());
    return std::move(result).Error();
}
// to check for success
if (ResultType::IsSuccess(result)){
    doSomething(result.CRefSuccess());
}

Sometimes when you do explicit checking on error and success, you realize you can put the action on success in a function and add it to the existing chain.

Documentation

This library makes use of an overloaded operator|(Argument, Function). To make this overload work, at least one of its arguments needs to be a non primitive type. A normal function will be passed as function pointer, which is a primitive type. Either at least Argument is a class/struct or you can wrap a lambda expression around the function.

Chaining function calls

To mitigate the limitation of operator overload we don't use int as value type:

#include <result_type.h>

using ResultType::operator|();

struct Integer{int value;}; // to make the overloaded operator|() work with normal functions
struct SomeType{...};

auto someComplexCalculation(Integer value)-> Integer{
    ...
}

auto someSimpleCalculation(Integer value)->Integer{
    ...
}

auto mostComplexCalculation(SomeType value)->Integer{
    ....
    return something;
}

auto calculationWithTwoParameters( double dependency, Integer integer)-> Integer{
    ...
} 

In this case nothing special is needed. You can do this without this library.

auto const result = someSimpleCalculation(someComplexCalculation(mostComplexCalculation(SomeType{2.0})))

But it is hard to read. When reading source code, you do this from left to right. To know what is happening there, you look for the most inner call, and go back to the left. In this case this library can improve the reading flow:

auto const result = SomeType{2.0} | mostComplexCalculation | someComplexCalculation | someSimpleCalculation;

When functions take more than one parameter

Chaining function calls only works when functions take one parameter. To make it work with functions which take more than one parameter, we need to write adaptors.

Option 1:

Write a function, which takes the dependency and returns a lambda expression, that takes the other parameter and calls the function with two parameters

auto calculationWith(double dependency){
    return  [dependency](Integer integer){
        return calculationWithTwoParameters(dependency, integer);
    };
}

auto insideSomeOtherFunction(){
    auto const dependency = 3.0;
    ...
    auto const result = SomeType{2.0} | calculationWith(dependency) | mostComplexCalculation | someComplexCalculation;
    ...
}

Option 2:

Use lambda exprsessions to bind the dependency.

auto insideSomeOtherFunction(){
    auto const dependency = 3.0;
    ...
    using std::placeholders::_1;
    auto calculationWithDependency = [dependency](Integer integer){ return calculationWithTwoParameters(dependency, integer);};
    auto const result = SomeType{2.0} | calculationWithDependency | mostComplexCalculation | someComplexCalculation;
    ...
}

Option 3:

Use std::bind, that creates the function object for you.

auto insideSomeOtherFunction(){
    auto const dependency = 3.0;
    ...
    using std::placeholders::_1;
    auto calculationWithDependency = std::bind(calculationWithTwoParameters, dependency, _1);
    auto const result = SomeType{2.0} | calculationWithDependency | mostComplexCalculation | someComplexCalculation;
    ...
}

Option 1 is the preferred way over Option 2 and 3, because Option 1 does not hide the dependency within the call chain.

When functions take/return std::optional<T>

Then using the operator|() will make sure the expression will be never return a nested std::optional<std::optional<...>>. No need to unpack nested optionals to get the result.

Chaining a function, which returns std::optional<T> with a function that takes std::optional<T>

In this case no special rule is applied. The function, which takes a std::optional<T>, is called regardless, if the passed optional holds a value or not. The result of this chain will be of the same type as the return type of the second function. Which seems obvious, but in later cases it will be important to pay attention to this.

#include <result_type.h>

using ResultType::operator|();


auto mayReturnNothing(SomeType value)-> std::optional<int>{
    ...
}

auto canDealWithGettingNothing(std::optional<int> value)->SomeOtherType{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'SomeOtherType'
    auto const result = value | mayReturnNothing | canDealWithGettingNothing;
    ...
}

Chaining a function, which returns std::optional<T> with a function that takes T and returns non optional U

In this case the second function gets called only and if only the returned value from the first function holds a value. If the first function call returns std::nullopt, std::nullopt will be returned for the whole expression. The result of this chain will be of type std::optional<U>.

#include <result_type.h>

using ResultType::operator|();


auto mayReturnNothing(SomeType value)-> std::optional<int>{
    ...
}

auto needsSomething(int value)->SomeOtherType{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'std::optional<SomeOtherType>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

Chaining a function, which returns std::optional<T> with a function that takes T and returns void

In this case the second function gets called only and if only the returned value from the first function holds a value. Otherwise std::nullopt is returned. The result of this chain will be of type std::optional<ResultType::NothingType>.

#include <result_type.h>

using ResultType::operator|();


auto mayReturnNothing(SomeType value)-> std::optional<int>{
    ...
}

auto needsSomething(int value)->void{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'std::optional<ResultType::NothingType>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

Chaining a function, which returns std::optional<T> with a function that takes T and returns std::optional<U>

In this case the second function gets called only and if only the returned value from the first function holds a value. If the first function call returns std::nullopt, std::nullopt will be returned. The result of this chain will be of type std::optional<U>. The operator|() flattens the return type. There will be no nested std::optional<std::optional<U>>

#include <result_type.h>

using ResultType::operator|();


auto mayReturnNothing(SomeType value)-> std::optional<int>{
    ...
}

auto needsSomething(int value)->std::optional<SomeOtherType>{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'std::optional<SomeOtherType>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

When functions take/return Result<T, E>

The type Result<T, E> is a sum type. It can only hold either T or E. Where T represents success and E an error. This library assumes that type E is the same type for all values. If errors of different types are needed, std::variant<ErrorType1,..., ErrorTypeN> can be used.

Using the operator|() will make sure the expression will be never return a nested Result<Result<...>, Error>. No need to unpack nested results.

Chaining a function, which returns Result<T, E> with a function that takes Result<T, E>

In this case no special rule is applied. The function, which takes a Result<T, E>, is called regardless, if the passed value holds a value or an error. The result of this chain will be of the same type as the return type of the second function. Which seems obvious, but in later cases it will be important to pay attention to this.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnError(SomeType value)-> ResultType<int, Error>{
    ...
}

auto canDealWithErrors(ResultType<int, Error> value)->SomeOtherType{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'SomeOtherType'
    auto const result = value | mayReturnError | canDealWithErrors;
    ...
}

Chaining a function, which returns Result<T, E> with a function that takes T and returns U

In this case the second function gets called only and if only the returned value from the first function holds a T. If the first function call returns a value holding E, E will be returned for the whole expression. The result of this chain will be of type Result<U, E>.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnNothing(SomeType value)-> Result<int, Error>{
    ...
}

auto needsSomething(int value)->SomeOtherType{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'Result<SomeOtherType, Error>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

Chaining a function, which returns Result<T, E> with a function that takes T and returns void

In this case the second function gets called only and if only the returned value from the first function holds a value of T. If it holds E the expression returns this error. The result of this chain will be of type ResultType::Result<ResultType::NothingType, E>.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnAnError(SomeType value)-> ResultType::Result<int, Error>{
    ...
}

auto needsSomething(int value)->void{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'ResultType::Result<ResultType::NothingType, Error>'
    auto const result = value | mayReturnAnError | needsSomething;
    ...
}

Chaining a function, which returns Result<T, E> with a function that takes T and returns Result<U, E>

In this case the second function gets called only and if only the returned value from the first function holds a value of T. If the first function call returns E, the call chain will be left and E will be returned. The result of this chain will be of type Result<U, E>. The operator|() flattens the return type. There will be no nested std::Result<Result<U, E>, E>

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnNothing(SomeType value)-> ResultType::Result<int, Error>{
    ...
}

auto needsSomething(int value)->ResultType::Result<SomeOtherType, Error>{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'ResultType::Result<SomeOtherType, Error>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

When functions take/return Result<std::optional<T>, E>

The type Result<T, E> is a sum type. It can only hold either T or E. Where T represents success and E an error. This library assumes that type E is the same type for all values. If errors of different types are needed, std::variant<ErrorType1,..., ErrorTypeN> can be used.

Using the operator|() will make sure the expression will be never return a nested Result<Result<...>, Error>. No need to unpack nested results.

Chaining a function, which returns Result<T, E> with a function that takes Result<T, E>

In this case no special rule is applied. The function, which takes a Result<T, E>, is called regardless, if the passed value holds a value or an error. The result of this chain will be of the same type as the return type of the second function. Which seems obvious, but in later cases it will be important to pay attention to this.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnError(SomeType value)-> ResultType<int, Error>{
    ...
}

auto canDealWithErrors(ResultType<int, Error> value)->SomeOtherType{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'SomeOtherType'
    auto const result = value | mayReturnError | canDealWithErrors;
    ...
}

Chaining a function, which returns Result<T, E> with a function that takes T and returns U

In this case the second function gets called only and if only the returned value from the first function holds a T. If the first function call returns a value holding E, E will be returned for the whole expression. The result of this chain will be of type Result<U, E>.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnNothing(SomeType value)-> Result<int, Error>{
    ...
}

auto needsSomething(int value)->SomeOtherType{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'Result<SomeOtherType, Error>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

Chaining a function, which returns Result<T, E> with a function that takes T and returns void

In this case the second function gets called only and if only the returned value from the first function holds a value of T. If it holds E the expression returns this error. The result of this chain will be of type ResultType::Result<ResultType::NothingType, E>.

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnAnError(SomeType value)-> ResultType::Result<int, Error>{
    ...
}

auto needsSomething(int value)->void{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'ResultType::Result<ResultType::NothingType, Error>'
    auto const result = value | mayReturnAnError | needsSomething;
    ...
}

Chaining a function, which returns Result<T, E> with a function that takes T and returns Result<U, E>

In this case the second function gets called only and if only the returned value from the first function holds a value of T. If the first function call returns E, the call chain will be left and E will be returned. The result of this chain will be of type Result<U, E>. The operator|() flattens the return type. There will be no nested std::Result<Result<U, E>, E>

#include <result_type.h>

using ResultType::operator|();

enum class Error{Fail1, Fail2};

auto mayReturnNothing(SomeType value)-> ResultType::Result<int, Error>{
    ...
}

auto needsSomething(int value)->ResultType::Result<SomeOtherType, Error>{
    ...
}

auto insideSomeOtherFunction(SomeType value){
    ...
    // result is of type 'ResultType::Result<SomeOtherType, Error>'
    auto const result = value | mayReturnNothing | needsSomething;
    ...
}

External resources

About

This a header only library which enables the library user to chain function calls in a readable fashion, that looks like piping commands in a shell script. While handling of errors is done implicitly.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published