Background: I'm working on a transducers proposal for Phobos,
which
generalizes ranges to support push data-flow model (in addition
to the current pull-only) enabling composable algorithmic
transformations that can be applied to other sources of data such
as RX observables[0], signal & slots, channels, etc, as well as
the traditional data sources like containers, generator ranges
like `iota` and so on.
For example, this would allow one to use a single transformation
object e.g. { filter -> map -> uniq } for both synchronous and
asynchronous sequences of data.
The existing implementations that I'm studying are the one
introduced originally by Rich Hickey in Clojure[1] and Ableton's
Atria C++ library presented at CppCon 2015 [2].
My main obstacle is that they both rely on returning polymorphic
lambdas. E.g. polymorphic lambdas are need to be first-class
value types.
AFAIU, as per proposal N3649[3] which was accepted for C++14, if
one of the parameters of a lambda expression has type-specifier
`auto`, the compiler should generate a **non-templated**
placeholder struct with a **templated** call operator. This
allows the following code to work:
```
// Compile with -std=c++14
#include <iostream>
#include <vector>
using namespace std;
int main()
{
double local_var = 1.0;
auto add = [&](auto number) { local_var += number; };
add(3); cout << local_var << endl; // Prints 4
add(3.14); cout << local_var << endl; // Prints 7.14
add(20LL); cout << local_var << endl; // Prints 27.14
auto vec = vector<decltype(add)> { add, add, add, add };
for (auto&& fun : vec)
{
fun(1);
fun(0.5);
fun(2LL);
}
cout << local_var << endl; // Prints 41.14
}
```
In D, on the other hand, the compiler stores a reference to the
lambda function as a function-pointer (or a delagate-pointer, if
any local variables are captured), however this precludes support
for polymorhpic lambdas. AFAIU, delegates are implemented the way
they are because this enables much easier ABI interoperability.
Kenji worked on supporting polymorphic lambdas for DMD 2.070
[4][5] and his design
(if I understand it correctly) works like so:
```
alias add = (a,b) => a + b;
// is syntactically lowered to
alias add = add__compile_generated;
template add__compile_generated(T, U)
{
auto add__compile_generated(T a, U b) { return a + b; }
}
```
This by itself was a good enhancement, however it is incapable of
handling some very useful cases which C++14 supports, showcased
in the below example, which is adopted from the Transducers
CppCon 2015 lecture:
```
// 0) accumulate
// (nothing special here)
template <typename In, typename S, typename Rf>
S accumulate(In first, In last, S state, Rf step) {
for (; first != last; ++first) {
state = step(state, *first);
}
return state;
}
// 1) output_rf
// !!! Proper value-type with templated operator() !
auto output_rf = [] (auto out, auto input) {
*out++ = input;
return out;
};
// 2) map & filter a value-types with templated operator()
// that return value-types with tempalted operator()
// !!!
auto map = [] (auto fn) {
return [=] (auto step) {
return [=] (auto s, auto ...ins) {
return step(s, fn(ins...));
};
};
};
// !!!
auto filter = [] (auto pred) {
return [=] (auto step) {
return [=] (auto s, auto ...ins) {
return pred(ins...)?
step(s, ins) :
s;
};
};
};
auto f = filter([](int x) { return x > 0; });
auto xf = comp(
filter([](int x) { return x > 0; }),
map([](int x) { return x * 2; })
);
accumulate(first, last, out, xf);
// which turns into:
accumulate(first, last, out, comp(filter(pred), map(fn))
(output_rf));
// which turns into:
accumulate(first, last, out, filter(pred) (map(fn) (output_rf)));
// In order to replicate this in D I need to use ugly workarounds
like:
// * wrap the lambda expression in a dummy struct
// * put the body of the lambda expression in a templated opCall
// * return DummyStruct.init, in order to prevent the compiler
from trying to call
// the opCall function
// * @property just in case some future version decides to
enforce if for
// calling functions without parentheses
```
```
// 1) translated to D
// current syntax
@property auto outputRf()
{
struct OutputReducer
{
Out opCall(OutRange, E)(ref OutRange output, E elem)
if (isOutputRange!(OutRange, E))
{
output.put(elem);
return output;
}
}
return OutputReducer.init;
}
// desired syntax:
auto outputRf(O, E) = (ref O output, E elem) {
output.put(elem);
return output;
};
// 2) translated to D
// current syntax:
auto map(alias fn, Rf)(Rf step)
{
struct MapResult
{
Rf step;
auto opCall(S, Input...)
(auto ref S state, auto ref Input inputs)
{
return step(state, fn(inputs));
}
}
MapResult result = { step : step };
return result;
}
// New problems:
// * Need to use alias template parameter to capture the mapping
function.
// It would be better if I could take it as a value parameter
like in C++.
// * Need to manually put all of the captured variables as
members of the struct
// * Can't return MapResult(step), because the compiler confuses
it with a call to
// opCall, even though opCall is not a static function. Instead
I need to use
// the C struct initializer syntax.
//
// Desired syntax:
// 1. Omitting the type-specifier let's the compiler know
// that there should be a corresponding template parameter.
// 2. Allow for optional template parameters before the
// run-time parameters.
auto filter = (pred) {
return (step) {
return (S, Inputs...)(S s, Inputs ins) {
return pred(ins)?
step(s, ins) :
s;
};
};
};
```
To sum up, I want the compiler to do all those things for me,
like the C++14 compilers do. Presently it feels like I'm doing
the compiler's job.
[0]: http://reactivex.io/intro.html
[1]: https://www.youtube.com/watch?v=6mTbuzafcII
[2]: https://www.youtube.com/watch?v=vohGJjGxtJQ
[3]:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3649.html
[4]: http://dlang.org/changelog/2.070.0.html#alias-funclit
[5]: https://github.com/D-Programming-Language/dmd/pull/3638