Type erasure

auto xf = /* ¿¿¿??? */ {};
if (...) {
    xf = filter([] (int x) { return x % 2; });
} else {
    xf = map([] (int x) { return x * 2; });
}

Consider this code example. What should the type for the transducer xf be?

These invocations of filter and map return objects of different types. This is part of what makes the library so efficient—all the information about the processing chain is assembled at compile time. However, in this case, neither is a suitable type for xf.

The solution is called type erasure, providing a type that removes some of the details, leaving the part that we are interested in. For example, std::function<Signature> allows us to take any function or callable object, regardless of its type, as long as they have a compatible signature. Similarly, this library provides a type transducer<Inputs [,Outputs]> that can hold any transducer that can take and produce inputs and outputs of the given types.


template <typename InputT = meta::pack<>, typename OutputT = InputT>
class zug::transducer

Type erased transducer.

This allows to store transducers in places where the full type can not be known at compile time. The InputT template argument is the type of the input over which you may apply the transducer. For example:

transducer<int> filter_odd = filter([] (int x) {
    return x % 2;
});

A second template argument can be passed to indicate the type of data after running through the transducer. By default, as in the previous example, it’s the same as the input, but it does not have to be:

transducer<int, std::string> serialize = map([] (int x) {
    return std::to_string(x);
});

Note that, both the first or second template arguments can take a meta::pack when it can take or pass more than one input type.

transducer<pack<int, int>, std::tuple<int, int>> xf1 = zip;
transducer<std::tuple<int, int>, pack<int, int>> xf2 = unzip;
transducer<pack<int, int>, float> xf3 = map([] (int a, int b) {
    return float(a) / b;
});

Like any other transducer, a type erased one can be composed with other transducers that have compatible signatures:

auto x1 = transducer<int>{...};
auto x2 = transducer<int, pack<float, std::string>>{...};
auto x3 = x1 | x2 | map([] (float x, std::string s) {
    return std::to_string(x) + s;
});
// Note that the result of composing type erased transducers is not a type
// erased one.  We can however still type erase the result:
auto x4 = transducer<int, std::string>{x3};

Warning

Type erased transducers do have a performance cost. Not only is it slower to pass them around, they are significantly slower during processing of the sequence. For such, use them only when really needed, and otherwise use auto and templates to avoid erasing the types of the transducers.

Note

A type erased transducer actually defers applying the held transducer until it first runs through a sequence, as ilustrated by this example:

transducer<int> filter_odd = [](auto step) {
    std::cout << "Building step" << std::endl;
    return [](auto st, int x) {
        return x % 2 ? step(st, x) : st;
    };
};
// Doesn't print anything
auto step = filter_odd(std::plus<>{});
// Now it prints
auto sum = reduce(step, 0, {1, 2, 3})

This should normally have no implications. All transducers in the library perform no side effects when applying a reducing function.