SkillAgentSearch skills...

Ftl

C++ template library for fans of functional programming

Install / Use

/learn @beark/Ftl
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

FTL - The Functional Template Library

Build Status

C++ template library for fans of functional programming.

This project is currently on hiatus. Not sure when/if I'll resume work on it.

If you want to play around with it anyway, you need to compile in C++11 mode with gcc-4.8 or later or clang-3.2 or later. Not sure if newer MSVC versions would work or not.

Tutorials

Showcases

A couple of quick showcases of some rather neat things the FTL gives you.

Curried Function Calling

One of the typically functional conventions brought to C++ by FTL is support for curried functions and function objects. A curried function is an n-ary function that can be invoked one argument at a time; each step returning a new function object of arity n-1. Once enough parameters have been applied (when n reaches 0), the actual computation is performed and the result is returned. One of the uses of this is to achieve very convenient partial function application. For example:

auto plus = ftl::curry(std::plus<int>);
auto addOne = plus(1);

auto x = addOne(2); // x = 3
auto y = addOne(x); // y = 4

As mentioned, all of the function objects provided by FTL are curried by default and do not require an ftl::curry call first. Partial application is thus in many cases extremely concise and clean. Note, however, that without, for instance, a wrapping lambda function, it is not possible to partially apply parameters other than in the exact order they appear. For example, the following are all valid:

auto f = curriedTernaryFn(1);
auto g = curriedTernaryFn(1,2);

f(2)(3) == f(2,3) && f(2,3) == g(3); // true

But it is not possible to "skip" a parameter or leave a placeholder, as in:

using std::placeholders::_1;
auto f = std::bind(ternaryFn, _1, 2, 3);

Currying by itself is a very nice thing to have, but what truly makes it shine is when used in combination with e.g. higher order functions. See for instance applicative style coding, which is not nearly as nice without currying.

Expanding The Standard Library

One of the nice things about FTL is that it does not try to replace or supercede the standard library, it tries to expand it when possible. These expansions include giving existing types concept instances for e.g. Functor, Monad, Monoid, and others. For example, in FTL, std::shared_ptr is a monad. This means we can sequence a series of operations working on shared pointers without ever having to explicitly check for validity—while still being assured there are no attempts to access an invalid pointer.

For example, given

shared_ptr<a> foo();
shared_ptr<b> bar(a);

We can simply write

shared_ptr<b> ptr = foo() >>= bar;

Instead of

shared_ptr<b> ptr(nullptr);
auto ptra = foo();
if(ptra) {
    ptr = bar(*ptra);
}

Which would be the equivalent FTL-less version of the above.

Monadic code may perhaps often look strange if you're not used to all the operators, but once you've got that, reading it becomes amazingly easy and clear. operator>>= above is used to sequence two monadic computations, where the second is dependant on the result of the first. Exactly what it does varies with monad instance, but in the case of shared_ptr, it essentially performs nullptr checks and either aborts the expression (returning a nullptr initialised shared_ptr), or simply passes the valid result forward.

Other types that have been similarly endowed with new powers include: std::future, std::list, std::vector, std::forward_list, std::map, std::unordered_map, std::set, and more.

Sum/Union Types With Pattern Matching

For those not familiar with sum types, they can be viewed as tagged unions. In other words, they are defined at compile time by a set of possible types, and at run-time their value will be of exactly one of those types at any particular instance. A quick example:

ftl::sum_type<std::string,int> x{ftl::constructor<int>(), 3};

Here, x, can take on any value that is either of type std::string or int and we've initialised it to be the integer value 3. It is necessary to use the constructor<T> tag to specify what type to initialise to, because some types may have constructors accepting similar or even the same arguments.

Once we have our sum type, we can now pattern match—or switch on its type, if you prefer—to get a guaranteed safe way of accessing its value:

int y = x.match(
    [](int x){ return x+1; },           // This function will be invoked, with
                                        // 3 as parameter

    [](const std::string&){ return 0; } // This function would have been invoked
                                        // if x was a string
);

Unlike in a language with first class support for pattern matching, in FTL we are unfortunately forced to use functions as match clauses, as you can see above. This makes it slightly less compelling, but we do get the same static guarantees at least: FTL checks at compile time that you've covered every possible type the matched sum type could take on.

Note that match must return a value by default, to encourage the functional way where everything is an expression and therefore returns a value. This means, of course, that all match clauses must return values such that std::common_type can find a common type they are all implicitly convertible to.

If the above expression-semantic of match is not actually desirable, there is also matchE, which does "effectful" matches:

x.matchE(
    [](int& x){ ++x; },
    [](const std::string& x){ std::cout << x; }
);

Another possible point of inconvenience is if we have a complicated sum type of many sub-types. It can be quite a bother to write out every match case explicitly then, especially if we're only interested in one or a couple. Enter otherwise:

ftl::sum_type<A,B,C,D> x{...};
auto r = x.match(
    [](const B& b){ return f(b); }, // This function will be invoked only if
                                    // x's value is of type B

    [](otherwise){ return g(); }    // This function will be invoked if x's
                                    // value is of any type except B
);

It is of course possible to have several specific match clauses before the otherwise-clause, but make sure to always put otherwise last—or else any clause appearing below it will never actually be executed. Simply put, matching is done in the order the expressions appear.

The true usefulness of sum_type isn't so much the case where you use it directly. It's when you use it to quickly and cleanly create new data types. For example, did you know both ftl::maybe and ftl::either are really just simple type aliases of sum_type? It's true, maybe is defined simply as:

template<typename T>
using maybe = sum_type<T,Nothing>;

That takes care of the vast majority of the logic required for maybe, though there is a number of convenience functions and definitions in addition.

Applying Applicatives

Adding a bit of the Applicative concept to the mix, we can do some quite concise calculations. Now, if we are given:

int algorithm(int, int, int);
ftl::maybe<int> maybeGetAValue();
ftl::maybe<int> maybeGetAnother();
ftl::maybe<int> maybeGetAThird();

where ftl::maybe is a type similar to e.g. boost::optional, provided by FTL (mostly to make sure there are no external dependencies save the standard library).

Then we can compute:

/* ftl::operator% is short for ftl::fmap, basically the classic "map" function
 * of functional programming.
 * Similarly, ftl::operator* is short for ftl::aapply, the work horse function of
 * applicative programming style. It basically applies a function (the left hand
 * side) to one argument at a time (the right hand side).
 */
using ftl::operator%;
using ftl::operator*;
auto result =
	ftl::curry(algorithm) % maybeGetAValue() * maybeGetAnother() * maybeGetAThird();

which would compute the result of algorithm, but only if every one of the maybe functions returned a value.

In other words, without Functor's fmap and Applicative's aapply, it would have looked something like:

ftl::maybe<int> result;
auto x = maybeGetAValue(), y = maybeGetAnother(), z = maybeGetAThird();
if(x.is<int>() && y.is<int>() && z.is<int>()) {
    result = ftl::value(algorithm(ftl::get<int>(x), ftl::get<int>(y), ftl::get<int>(z)));
}

If algorithm had happened to be wrapped in an ftl::function, or else be one of the built-in, curried-by-default function objects of FTL, then the curry call could have been elided for even cleaner code.

Exactly what operation is done by apply varies from type to type. For example, with containers, it generally implies combining their elements in every possible combination. Thus

curry(std::plus<int>) % std::list<int>{1,2} * std::list<int>{5,10};

can be read as "for each element in {1,2}, combine it using std::plus<int> with each element in {5,10}", resulting in the list {6, 11, 7, 12}. Of course, what happens on a more technical level is that std::plus<int> is partially applied to each element in the first list, resulting in a list of unary functions, that in turn gets applied to each element in the second list.

A much less technical way of thinking—to gain some intuition for how applicative expressions behave—could be that operator% is an alias for function application, and operator* separates parameters. Except the parameters happen to be wrapped in some "context" or "container".

This type of function application scales to arbitrary arity.

Transformers

No, not as in Optimus Prim

Related Skills

View on GitHub
GitHub Stars992
CategoryDevelopment
Updated1mo ago
Forks66

Languages

C++

Security Score

95/100

Audited on Feb 26, 2026

No findings