Dobiasd / FunctionalPlus

Functional Programming Library for C++. Write concise and readable C++ code.
http://www.editgym.com/fplus-api-search/
Boost Software License 1.0
2.09k stars 167 forks source link

Leveraging C++ 14 features for forward function application (unix pipe style). What is your opinion on this? #58

Closed Dobiasd closed 7 years ago

Dobiasd commented 7 years ago

Let's say we have the following three functions

const auto times_3 = [](int i){return 3 * i;};
const auto is_odd = [](int i){return i % 2 == 0;};
const auto as_string_length = [](int i){return std::to_string(i).size();};

and want to transform/filter a sequence of numbers with them and sum up the result.

Up to now, we would have to write the following:

const auto result_old_style =
    sum(
    transform(as_string_length,
    drop_if(is_odd,
    transform(times_3,
    numbers(0, 15000000)))));

I do not like the fact that we have to read backwards to perceive the flow of data. I think something in the style of unix pipes would be nicer.

With some additional code in the library we could do the following thing:

const auto result_new_style = fwd::apply(
    numbers(0, 15000000)
    , fwd::transform(times_3)
    , fwd::drop_if(is_odd)
    , fwd::transform(as_string_length)
    , fwd::sum());

Furthermore it would solve the problem of the almost unusable-ugly function composition, going from this:

const auto function_chain_old_style = compose(
    bind_1st_of_2(transform<decltype(times_3), std::vector<int>>, times_3),
    bind_1st_of_2(drop_if<decltype(is_odd), std::vector<int>>, is_odd),
    bind_1st_of_2(transform<decltype(as_string_length), std::vector<int>>, as_string_length),
    sum<std::vector<std::size_t>>);

to that:

const auto function_chain_new_style = fwd::compose(
    fwd::transform(times_3),
    fwd::drop_if(is_odd),
    fwd::transform(as_string_length),
    fwd::sum());

Transforming over the inner containers in nested ones

typedef std::vector<int> ints;
std::vector<ints> nested_ints;

would thus also become simpler, going from:

const auto nested_transformed_old_style = transform(
    bind_1st_of_2(transform<decltype(times_3), ints>, times_3),
    nested_ints);

to

const auto nested_transformed_new_style = fwd::apply(
    nested_ints
    , fwd::transform(fwd::transform(times_3)));

It would increase the compiler requirements from C++11 to C++14. But this functionality could be provided in a separate header, so one could still use the old style when only C++11 is available. Here is the full code of the example above. (The code up to line 104 would live in a file perhaps called fwd.hpp.)

What do you think about this idea?

astrodroid commented 7 years ago

This does sounds interesting and much cleaner, at least for the bind functions. For the first example I actually prefer the current order of operations than fwd::apply as that's how you would write that statement if it was an equation. But I like the way that you chain the commands in that case.

Given that the return type is already known to the complier as it is what returned by the lamda, wouldn't it be possible to use something like this to maintain c++11 compatibility if needed as done in the bind functions?

// API search type: bind_1st_of_2 : (((a, b) -> c), a) -> (b -> c)
// Bind first parameter of binary function.
template <typename F, typename T,
    typename FIn0 = typename utils::function_traits<F>::template arg<0>::type,
    typename FIn1 = typename utils::function_traits<F>::template arg<1>::type,
    typename FOut = typename utils::function_traits<F>::result_type>
std::function<FOut(FIn1)> bind_1st_of_2(F f, T x)
Dobiasd commented 7 years ago

@astrodroid Thanks for the feedback.

Yes, an equation would have the "old style". But while working with Elm and F#, I learned to love the |> operator, because the reading feels more fluent to me, especially in longer chains.

I'm not sure if I understand exactly what you mean. But let's take fwd::transform(times_3) as an example. To deduce the return type of this by using the return type of the lambda (times_3 in that case) would only be possible if the container type would also be known. But fwd::transform(times_3) returns a generic function (working with std::vector, std::list), and I think returning something like this is not possible in C++11. But if you find a way, I would be very excited. :)

astrodroid commented 7 years ago

@Dobiasd Ok I didn't realise that was the case.

No no ideas, at most you can disable that header for C++11 with a #define.

Dobiasd commented 7 years ago

That is a very good idea. This way one does not need to include the planned fwd.hpp explicitly. It will be simply only be included #if __cplusplus >= 201402L. Thanks. :+1:

Right now I'm continuing to implement the new stuff. During this process I found a funny gcc bug btw. ;)

Dobiasd commented 7 years ago

It's done. :) The API did only break in a few minor places. I had to change the order of some template parameters and function parameters at some places to make the partial currying in namespace fwd possible. The Macro invocations are generated automatically. During the next days, I will write a short description of the new possibilities in the README.md. Also I think I will add a CONTRIBUTION.md to the repo, to help us consolidate the high-level knowledge. Magic comments like // API search type... and // fwd bind count... need an explanation I guess. ;)

Dobiasd commented 7 years ago

README.md is updated and CONTRIBUTION.md was added.

I'm excited to see how useful the new features will render itself during the daily usage at our company.

offa commented 7 years ago

The contribution.md seems a bit long and detailed to me. I guess it makes more sense to keep it short or at least some short info (eg. https://github.com/Microsoft/GSL/blob/master/CONTRIBUTING.md)

Dobiasd commented 7 years ago

OK. Do you have a suggestion where this kind of library documentation would fit?

offa commented 7 years ago

Hmmm, what about a quick overview at the beginning? Something like a tl;dr? The full text can be included afterwards since it contains many useful infos.

Something like this structure:

Contributing to fplus

1 or 2 sentences what fplus is intended to do.

Issues

Things related to Issues, eg. include compiler / version, platform etc.

Pull requests

What PR's should include:

  • Description
  • Up-to-date to master (useful?)
  • Test cases included
  • Informative commit message

    Full text goes here …

astrodroid commented 7 years ago

As @offa said that just an overview of the project.

But just as important first ask the potential contributors to report an issue first, to avoid duplication with something that was already planned, or maybe avoid a 1000 line surprise commit request :)))

offa commented 7 years ago

At least for new features, but yeah, totally makes sense.

offa commented 7 years ago

We also should move this to an extra issue. Not really C++14 related! 😄

offa commented 7 years ago

Opened one: #62

Dobiasd commented 7 years ago

This commit extended the API search to also show the function signatures from namespace fwd. Additionally they are also considered for search results.

So now it shows:

repeat : (Int, [a]) -> [a]
fwd::repeat : (Int) -> [a] -> [a]
repeat(3, [1, 2]) == [1, 2, 1, 2, 1, 2]

template <typename Container>
Container repeat(std::size_t n, const Container& xs)

I hope this helps to use the new functionality. At least I can not remember all the functions we currently have, and use the API search almost every day.

Btw, the new signatures in the search results are generated automatically in the frontend, which was quite easy to implement, thanks to the parsing that was done already. 8-)