cplusplus / sender-receiver

Issues list for P2300
Apache License 2.0
18 stars 3 forks source link

consider rename `execution::just` #122

Open ericniebler opened 9 months ago

ericniebler commented 9 months ago

Issue by huixie90 Wednesday Oct 27, 2021 at 11:37 GMT Originally opened as https://github.com/NVIDIA/stdexec/issues/223


Sorry if this is not the right place to send feedback from the "general public".

The name execution::just is a little bit miss leading for a functional programming developer. Usually just is associated with Maybe. In C++ context, I'd expect just to create a std::optional.

In this case, I think the execution::just is taking a value and creating a sender with the value. This sounds like that execution::just is the implementation of pure for applicative functor or the return implementation for a monad.

ericniebler commented 9 months ago

Comment by ericniebler Thursday Oct 28, 2021 at 18:55 GMT


FWIW, I agree with this point. I think it's called "just" right now because that has some precedence in Rx, but s/r is not an Rx library. I think the precedence in FP takes precedence, so to speak.

In this case, I think the execution::just is taking a value and creating a sender with the value. This sounds like that execution::just is the implementation of pure for applicative functor or the return implementation for a monad.

Just so. We obviously can't call it return, and pure is also loaded in imperative programming languages. In the past, I have pushed for ready, which has some precedence.

ATTN: @kirkshoop

ericniebler commented 9 months ago

Comment by kirkshoop Friday Oct 29, 2021 at 18:45 GMT


I don't like just() I don't like pure() because it is not describing the operation it is describing a property of the operation that is not unique to this operation. I don't like return() because it is a keyword in C++ and does not describe the operation IMO. I don't like ready() because it is not describing the operation it is describing a property of the operation that is not unique to this operation.

Naming is hard.

If we must change the name, I would like to submit datum() for consideration. I was thinking that this was like iota() but singular.

da·tum /ˈdādəm,ˈdadəm/ noun 1. a piece of information. "the fact is a datum worth taking into account" 2. a fixed starting point of a scale or operation. "an accurate datum is formed by which other machining operations can be carried out"

ericniebler commented 9 months ago

Comment by huixie90 Saturday Oct 30, 2021 at 21:19 GMT


@kirkshoop Why ready is not describing the operation? In the old future world, we have this https://en.cppreference.com/w/cpp/experimental/make_ready_future whose name is very obvious

I am listing more functions and the property of these functions:

then : Functor's fmap I think the name then is great.

just : Monad's return I like ready. datum is a bit like iota, which doesn't really mean too much to the developer and the developer has to know what it is to understand what it is.

let_* : Monad's chain, or mbind, or >>= I don't like let very much because let in functional programming languages is usually associated with let-expression

ericniebler commented 9 months ago

Comment by kirkshoop Sunday Oct 31, 2021 at 14:08 GMT


Why ready is not describing the operation? In the old future world, we have this https://en.cppreference.com/w/cpp/experimental/make_ready_future whose name is very obvious

It does produce a sender that will complete inline. There will be other senders that complete inline, must all of them include ready in the name? Ready state has nothing to do with stored values that are sent when start is called. just(), just_done(), just_error() refers to the data not the implementation detail shared with many other algos. I could add an overload of just() that takes a scheduler and a pack of values and just() still matches the implementation, but ready() would not, unless the scheduler was the inline scheduler.

then : Functor's fmap

I think the name then is great.

I am not a fan, but it does match its purpose. map or transform make more sense to me. map, because it is common in other libraries and transform because that was the name chosen for the STL equivalent for sequences. using a name that matches future libraries is misleading IMO, future is eager and tends to be always async, sender is lazy and it is common for a sender to be sync. IMO, transform and map make it easier to transfer knowledge from other libraries in other languages.

just : Monad's return

I like ready. datum is a bit like iota, which doesn't really mean too much to the developer and the developer has to know what it is to understand what it is.

I agree about datum being esoteric. some, just, generate (for a value, to match generator for sequences), and values, are all less esoteric and still mostly accurate to the functionality. We disagree about ready..

let_* : Monad's chain, or mbind, or >>=

I don't like let very much because let in functional programming languages is usually associated with let-expression

I am sympathetic to this. I'll let @lewissbaker weigh in on let_

ericniebler commented 9 months ago

Comment by huixie90 Monday Nov 01, 2021 at 11:10 GMT


@kirkshoop

just(), just_done(), just_error() refers to the data not the implementation detail

This is a great point and I totally agree. But you've listed all three possible states of stored data, and all of their names contain the world just, which makes the word just a bit redundant. Why don't we just remove the word just from their names?

"But if we remove word just from the function just, it will be an empty name", asked Bob

I think by just(x), you really mean just_value(x). if we remove just, we can call it value.

"But value, done, error on their own are overly overloaded names", asked Dave

Perhaps we can put them into a namespace. In std::ranges library, those view factories and view adaptors are put inside the namespace std::views. Can do we something similar here, i.e. put sender factories and sender adaptors into a namespace std::senders. So similar to std::views::xxx creates a std::ranges::xxx_view, we can have std::senders::xxx to create a std::execution::xxx_sender (or unspecified if we want to give the flexibility to the implementations). so here we can call them:

std::senders::value
std::senders::done
std::senders::error

"There will be other senders that will complete with value/done/error, must all of them include value/done/error in the name?", asked Kevin

Well, no. Let's look at std::ranges again. We have std::views::empty to create a view with no elements and std::views::single to create a view with one element. Of course, we have views that can have 0 element (a default constructed std::string_view for example) or one element. We don't need to add empty or single into their names.

"Why do you keep referring to std::ranges?

That is a great library created by some random guy on the internet. I hope one day he will read this post and make the range-v3 repo active again

ericniebler commented 9 months ago

Comment by lewissbaker Monday Nov 01, 2021 at 12:24 GMT


let_* : Monad's chain, or mbind, or >>= I don't like let very much because let in functional programming languages is usually associated with let-expression

I am sympathetic to this. I'll let @lewissbaker weigh in on let_

The intention was for let() to actually be equivalent to a variable declaration in a coroutine. e.g.

auto x = co_await foo();
co_return co_await bar(x);

should be roughly equivalent to:

let_value(foo(), [](auto& x) { return bar(x); })
ericniebler commented 9 months ago

Comment by kirkshoop Monday Nov 01, 2021 at 21:50 GMT


The intention was for let() to actually be equivalent to a variable declaration in a coroutine.

I think that it can be argued that this is an implementation detail that is required in order to perform the primary concern, which is to be the general form of 'transform' that can emit value packs and even a completly different signal.

ericniebler commented 9 months ago

Comment by lewissbaker Wednesday Nov 03, 2021 at 02:15 GMT


My main intention for the let algorithm was not a general form of transform (although it is indeed a more general form of transform) but rather as a tool for keeping the result of one sender alive while another sender that depends on those values is executing. i.e. to introduce a lifetime scope that is asynchronous.

ericniebler commented 9 months ago

Comment by kirkshoop Wednesday Nov 03, 2021 at 04:09 GMT


Yes, I do agree that was your intent and that it motivated the current name. I agree that it is a name that is coherent with the intent.

Based on my experience in other systems, I believe that names that describe the data and signal movement through an algorithm are a better fit then names that describe an implementation-strategy or implementation-detail.

I prefer transform() and map() to then() because then() describes the ordering of procedures in time, which while accurate is not describing the data manipulation and signal mapping.

I prefer value_from_done() to upon_done() for the same reason.

I prefer value_to() or sender_from_value() to let_value() because to me the data and signal flow is absent from the name let_value().

Anyway. Names will be changed in LEWG at some point. So I have been content to keep names stable for now. Perhaps that will turn out to have been a mistake.

ericniebler commented 9 months ago

Comment by villevoutilainen Wednesday Nov 03, 2021 at 06:48 GMT


Anyway. Names will be changed in LEWG at some point. So I have been content to keep names stable for now. Perhaps that will turn out to have been a mistake.

It's a terrible mistake to operate based on the expectation that LEWG will change the names. They will, given half a chance, so what you need to do as proposal authors is to not give them that chance.

Bake your names, make them coherent, decide them with rationale, and document that rationale. Otherwise your paper will end up being delayed for a discussion cycle or two to discuss names from a clean slate, and you're going to miss the deadlines you strive for. Or worse - you'll end up with names decided in a 5-minute beauty pageant with no rationale whatsoever.

ericniebler commented 9 months ago

Comment by Mrkol Wednesday Dec 29, 2021 at 01:51 GMT


I second the idea that just must mention value in it's name to specify the channel the signal will use. The motivation is as follows. When working with vulkan, not all asynchronous computations can (sometimes should) be waited for from the CPU side, even though they do produce values on the GPU. They first have to be chained with other operations that can be waited for. Therefore the value channel is not applicable for those types of computations, a new channel is needed that signals that the computation was started successfully and the next operation in a chain can be dispatched. Same sort of logic applies to optimization of io_uring operations: they can be chained and/or grouped in a more efficient way without awaiting for the intermediate computation to produce any values. E.g. let_value(when_all(write1, write2, ...), sync) can be optimized to use a single syscall afaik. Adding a "just" sender to that group of writes cannot be done via the value channel in case of such optimizations, as other writes do not actually produce any values. To be precise, we can use just_value, but that would presumably disable the optimized overload as one of the operations wouldn't be an io_uring one.

So generally speaking, we cannot assume that all senders/receivers use the value channel, hence robbing it of it's default/implicit status. Channels have to be spelled out explicitly so that confusion does not arise in advanced applications.

P.S. How about sync_value, sync_error and sync_done?

ericniebler commented 9 months ago

Comment by LeeHowes Wednesday Dec 29, 2021 at 19:23 GMT


I'm not sure what sync as a prefix is meant to suggest.

Renaming just seems reasonable. The idea that GPU tasks shouldn't complete with set_value is a dubious justification, though. If not completing with the success signal, what should GPU tasks complete with? Clearly in customisation chains they should skip that set_value call if they can, but in the absence of a customisation chain we aren't signally that a task has started, we are signalling that it has completed. set_value is the right signal for that.

ericniebler commented 9 months ago

Comment by Mrkol Wednesday Dec 29, 2021 at 22:57 GMT


sync as a prefix means that we construct a computation that synchronously completes with the specified signal and arguments.

About the GPU stuff, I was mistaken. There are no operations that can be chained but not awaited from the CPU. With like 1 exception (present operations cannot be neither waited for nor chained), all operations can both be chained GPU-side and waited CPU-side, although a properly designed system should wait CPU-side only once per frame, at the end of a big chain. Ensuring that the optimization happens is critical, therefore it might make sense to wrap operations into senders that cannot signal value to enforce proper usage.

If not completing with the success signal, what should GPU tasks complete with?

started signal with a handle to a GPU-side synchronization primitive that can be used to manually chain it. Although only practice can show whether this actually is a good idea or not.

In any case, all of this stuff is to be worked out in the coming years. Right now the point is that assuming value as a default in naming conventions might backfire long-term, so it should be specified explicitly.

ericniebler commented 2 months ago

What about with_value, with_error, and with_stopped?

auto work = with_value(1) | then([](int i) {...});

with_value(1) can also serve double-duty as an adaptor that adds an additional value to the value channel:

auto work = with_value(1) | with_value("hello") | then([](int, const char*) {...});

(obviously in the above expression with_value(1) | with_value("hello") can be shortened to with_value(1, "hello"), but the predecessor sender can be anything.)