Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should we replace send/receive with some other verbs? #1125

Open
njsmith opened this issue Jun 26, 2019 · 6 comments
Open

Should we replace send/receive with some other verbs? #1125

njsmith opened this issue Jun 26, 2019 · 6 comments

Comments

@njsmith
Copy link
Member

njsmith commented Jun 26, 2019

Splitting this discussion off from #796, and specifically the subthread starting here

@njsmith:

On another note: as I write receive_exactly and receive_until and receive receive receive, I'm somewhat regretting the decision to name our primitives send/receive. I do like the emphasis that these are active transfers "in the moment", versus read/write that refer to modifying passive media – and also it's good to avoid associations with Python's conventions around read, which are really inappropriate for Streams and for Channels, for different reasons. But receive is just an awkward word: kinda long, annoying to spell. I'd be tempted by get/put, except then we'd have Stream.get_some and that seems like a bad idea. give/take? put/take? put_all/get_any? Or just stick with send/receive... Maybe this should be a separate issue.

@smurfix:

WRT put/get: Umm … thinking about it, put/get works for single messages, while send/receive makes more sense for byte/character streams. This is because I associate put/get with things like channels or queues that you can use to put some single thing in on one end and get it out the other end.
send/receive on the other hand is for bytestreams and similar where the focus of these methods is on partitioning the stream into semantic chunks.

Calling receive_exactly on four bytes (and then N bytes) to de-partition a byte-encoded message > stream makes sense.
Calling get_exactly to read N HTTP requests or MQTT messages? not so much.

Yes, receive is awkward. So … use recv instead?

@njsmith:

thinking about it, put/get works for single messages, while send/receive makes more sense for byte/character streams. This is because I associate put/get with things like channels or queues that you can use to put some single thing in on one end and get it out the other end.

That's why I find them attractive :-). For single objects we currently have Channel.send and Channel.receive, and for byte streams we have Stream.send_all and Stream.receive_some. So in all cases, the verb on its own is being used to refer to single objects, and then sometimes we have a modifier to make it plural.

And get_exactly would take a Stream specifically, not a Channel, so I don't think there'd be any confusion about using it for MQTT.

@njsmith
Copy link
Member Author

njsmith commented Jun 27, 2019

Note: I was only about 20% convinced this was a good idea when I made the initial post, and after thinking about it some more I'm still only about 30% convinced, but since the number has gone up rather than down I figured we might as well have an issue to make sure it doesn't get forgotten.

My biggest hesitations are:

  • We need to stabilize as soon as we can and stop fiddling with stuff like this.
  • It's hard to judge a "feel" change like this without living with it for a bit.
  • Given the above two points, there's a risk that this is too small to be worth alienating users, or even that we'll do it, decide we don't like it after all, and then have to go through another transition to switch back.

That said, I feel like the best so far is put_all/get_any, so we'd have:

PutChannel.put
GetChannel.get
Channel.put, Channel.get

PutStream.put_all
GetStream.get_any
Stream.put_all, Stream.get_any

get_exactly(...)
get_until(...)

...and so on.

I feel like read/write would have the lowest weirdness-quotient:

ReadChannel.read
WriteChannel.write
Channel.write, Channel.read

WriteStream.write_all
ReadStream.read_any
Stream.write_all, Stream.read_some (or Stream.read_any)

read_exactly(...)
read_until(...)

...except that we normally think of read/write as pushing variable-length buffers, and here we'd specifically not be using them for that – the pattern is that bare read/write would only refer to single objects, while reading/writing an entire byte-string needs some description of what's happening to the individual bytes (write_all, read_some).

I guess we could use get/put for channels and read_<suffix>/write for streams, but it feels weird to have two totally different verbs for the same basic concept.

This also relates to the discussion over how similar streams and channels should be – see #959. If we did rename Stream to ByteChannel or similar, then it would be really weird for them to have different verbs. And even if we don't, I do like the idea of emphasizing that streams are basically channels where the units are individual bytes – this is a huge tripwire for people starting to work with networking, that I really don't think other projects have solved.

@smurfix
Copy link
Contributor

smurfix commented Jun 27, 2019

Meh.

Yeah, streams are channels of individual bytes, but I don't think of them that way. For me, streams are, well, a sequence of bytes that can be individually chopped up into arbitrary chunks that I write (when buffered) and read back (always). Thus using get/put for the one and read_*/write for the other doesn't feel at all strange to me.

In fact I'd avoid using the same name: people would assume Stream.put to send a bytearray as-is to the other side (which tends to work when you're running a local test, but not in real life). in fact they do that already when writing pipe or TCP protocols, cf. reurring StackOverflow questions along these lines, and anything we can do to not allow that problem to become even worse is a Good Thing IMHO.

@njsmith
Copy link
Member Author

njsmith commented Jun 27, 2019

anything we can do to not allow that problem to become even worse is a Good Thing IMHO

Yeah, but that's the whole reason I want to use the same verb :-).

The conjecture is, maybe people will understand this subtle point if we approach it like:

  1. First teach Channel.put
  2. Then teach ByteChannel.put_all, emphasizing that this is a channel that carries individual bytes, and if you have a bunch of bytes then put_all will let you put all of them in a row very efficiently.
  3. Then to drive that point home, we show these two animations right next next to each other: Channel[bytes] versus ByteChannel

That could be wrong, or it could that there's another even better way to tackle this common confusion, but this seems like one plausible approach that's also concrete enough to do.

@oremanj
Copy link
Member

oremanj commented Jul 8, 2019

One distinction that comes to mind: Typical usage of send and receive involves a peer you're communicating with, so the things you send aren't immediately going to show up on receive except in unusual cases. OTOH, typical usage of put and get (such as in the standard library's Queue object) is dealing with a single internal buffer, such that whatever you put does immediately show up as available for get. I'd be a little worried about confusion if we provide the former semantics with the latter names.

Also: I still hold out some hope that Trio will get a single queue-style object with put and get, because in simple cases dealing with each memory channel endpoint separately gets really cumbersome. And since there was concern that a "loopback channel" would create confusion between send/receive loopback vs send/receive on a bidirectional channel with a separate peer, I want different names available for the loopback case. get/put have precedent in the stdlib/asyncio/etc.

I'm fine with abbreviating receive to recv; it's easier to spell and shorter to type, but more UNIX-y cryptic and thus probably less user-friendly. I'm not sure how we should weight convenience for long-term users against comprehensibility for newcomers.

@himtronics
Copy link

@njsmith:

...except that we normally think of read/write as pushing variable-length buffers

so what about push/pull then?

@njsmith
Copy link
Member Author

njsmith commented Jul 30, 2019

@oremanj

One distinction that comes to mind: Typical usage of send and receive involves a peer you're communicating with, so the things you send aren't immediately going to show up on receive except in unusual cases. OTOH, typical usage of put and get (such as in the standard library's Queue object) is dealing with a single internal buffer, such that whatever you put does immediately show up as available for get

I think this will be OK in practice, because generally either you're communicating with someone in-process using a memory buffer, in which case anything you send does immediately show up to be received, or else you're communicating between processes, in which case you can't tell whether it shows up immediately or not. Actually even locally, the only way to tell is through _nowait methods, right? And a channel that doesn't have meaningful synchronous semantics won't offer _nowait methods.

Also: I still hold out some hope that Trio will get a single queue-style object with put and get, because in simple cases dealing with each memory channel endpoint separately gets really cumbersome. And since there was concern that a "loopback channel" would create confusion between send/receive loopback vs send/receive on a bidirectional channel with a separate peer, I want different names available for the loopback case. get/put have precedent in the stdlib/asyncio/etc.

We'll have this whether we want it or not, because as soon as we implement a bit of basic infrastructure it becomes a trivial one-liner:

def open_queue(buffer_size):
    return StapledChannel(*open_memory_channel(buffer_size))

And as a bonus, you can still do closure tracking – just write async with queue.send_channel: ... and async with queue.receive_channel: ....

But given how trivial this is, I think (a) we probably don't want to also have a separate Queue class that duplicates the functionality with more awkward closure tracking, (b) that means the method verbs will be the same for "queues" and "channels".

I think I'm OK with all that – it does relegate this to an idiom rather than a built-in concept, but that feels about right.

@himtronics

so what about push/pull then?

Seems workable too. Trying to come up with reasons that this might be better or worse than put/get:

  • push/pull are 4 characters to type versus 3 for put/get
  • I guess we get the option to choose between pull_any and pull_some, while get is stuck with get_any to avoid unfortunate connotations. (Of course in some English dialects, "pull" also has unfortunate connotations, but I'm guessing that's a different enough context that it's fine.)
  • push/pull are a bit less generic... there are a lot of classes out there with get methods that are unrelated to streaming data, like, all dict-like classes, or stuff like multiprocessing.Value (which has get and set methods). Of course there are also stacks with push/pop.
  • put/get matches the Python queue interface, which is both a positive and a negative, since our interface will be very similar to queue (suggesting we should use the same verbs), but not quite the same (suggesting we should use different verbs). I don't think most Trio programs will want to use queue.Queue or asyncio.Queue, so you probably won't see them next to our channels in the same codebase very often anyway.
  • push/pull are often used to refer to different strategies for moving data – whether it's driven by the sending side or the receiving side. For example, see Twisted's push producers and pull producers. If we do use push and pull then probably some minority of new users will bring in those preconceptions and jump to inaccurate conclusions. But some minority of new users will get confused no matter what we do, so I'm not sure if we should worry about that or not. I guess the worst part won't be the method names themselves, but the classes named PushChannel and PullChannel.

These all seem pretty minor and go both ways, so it's probably one of those things that's ultimately a matter of taste rather than anything objective.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants