Closed brson closed 10 years ago
I suggest:
Chan::open() -> (Sender, Receiver)
I agree with with @bascule. let (sender, receiver) = Chan::open()
feels natural to me.
To clarify, the reason I prefer Sender and Receiver is that port/chan are words that don't carry enough meaning to disambiguate without referencing the manual.
An orthogonal, but related proposal:
let sender = do spawn |receiver| {
}
It would presumably use a new name, not spawn
.
Huzzah for this bug! Boo for paired terminology without mnemonics.
Copying my post on the mailing list here:
The aversion to a standalone functions in this thread and in our community in general is concerning. Not everything has to be a method or associated function! There is no single thing being constructed here - both ends of the channel are being constructed at once.
Whatever names we choose for the sender and receiver ends of the pipe/channel, it would be nice to have the sender first in the tuple and the receiver last. Here is an ASCII diagram to help illustrate why I think this ordering makes sense:
channel() -> (Sender, Receiver)
Sender::send(T) >== channel ==> Receiver::recv() -> T
Not saying that Sender
, Receiver
or channel()
are the names we should pick - just illustrating how a well designed API can help reinforce the fact that the message is ‘sent’ down one end, and ‘received’ once it pops out the other.
The other thing we must consider is how the type names of the two ends of the pipe would be reflected in client code. Sender
and Receiver
when combined with the send
and recv
method names tend to look stuttery if you use the type names for the identifiers, :
let (sender, receiver) = channel();
sender.send(x);
let y = receiver.recv();
@bjz it's too bad there's no Call trait (yet?).
let (send, receive) = channel();
send(x);
let y = receive();
My thoughts:
Type::new -> Type
, unless there's a really good reason to not conform. In this case, I think Chan::open()
makes at least as much sense as Chan::new()
, so I'm not a huge fan of Chan::new()
. But:(Sender, Receiver)
.Out of these options, my vote goes to channel() -> (Source, Sink)
or channel() -> (Source, Drain)
.
I agree with @tikue that open
is preferable to new
. I disagree with the need to get more "personal" here. I prefer excruciatingly readable to clever or cute.
I would also add that we should pick one name out of 'stream', 'channel', or 'pipe' for the relationship between the sender and receiver, then purge the other names from the documentation. Using multiple names can be very confusing.
I like channel()
@bjz agreed. I would leave pipe
for the Unix API. I prefer channel
to stream
primarily because "stream" is pretty overloaded and "channel" mostly means this concept across PLs.
I think "channel" is definitely the correct term due to its heritage in CSP:
I'm also ok with channel()
or Channel::open()
.
Yes, I would look to the terminology most in use historically for the thing that most fits with our semantics. CML, which has a messaging model extremely similar to Rust's uses the term 'channel'. http://en.wikipedia.org/wiki/Concurrent_ML
let val c : string chan = channel ()
in ... end
Note that CML's channel primitives are bidirectional, hence the single result returned by the channel
function.
From 3.3.1 in Concurrent Programming in ML, Reppy writes:
Channels in CML are bidirectional; a thread that has access to a channel can both send and receive messages on it. A safer discipline would be to distinguish between input and output accesses to a channel, so that threads could be restricted to only send or receive on a channel. Another way to think of a channel is as a directional pipe, with each end represented by a value of a different type. While such a view of channels could be built into the language, in CML we can use events to provide this different style of channels as a first-class abstraction. The interface signature for directional channels is given in Listing 3.5. Values of the
in_chan
type can only be used to read messages, and those of theout_ch
type can only be used to send messages.
(* Listing 3.5: Directional channels signature *)
signature DIR_CHAN =
sig
type ’a in_chan and ’a out_chan
val channel : unit -> (’a in_chan * ’a out_chan)
val recv : ’a in_chan -> ’a
val send : (’a out_chan * ’a) -> unit
val recvEvt : ’a in_chan -> ’a event
val sendEvt : (’a out_chan * ’a) -> unit event
end;
Is there an inherit reason why there needs to be a separation between a Sender and a Receiver (or whatever the chosen terms are)? It seems like it'd be more intuitive to have a single bidirectional channel that handles both, with appropriate methods for each action.
let chan = channel();
This seems to be working great for Clojure, within the core.async library.
@TheHydroImpulse: That's what many languages do, CML, Erlang, Go, etc, but as I quoted above, even Reppy, the designer of CML says, "A safer discipline would be to distinguish between input and output accesses to a channel, so that threads could be restricted to only send or receive on a channel." It would be interesting to hear from @larsbergstrom whether he knows why bidirectional channels were chosen as CML's primitives (he has more experience with CML than I).
Of course it seems to be relatively easy to build directional channels on top of bi-directional channels and vice versa. I think from memory we have a Woops SyncChan
in extra::comm
. The issue with that is they can't be cloned easily now that SharedPort
was removed.SyncChan
is directional too (facepalm). I do think it would be easy to create one from our current primitives though.
use std::comm::{Port, Chan};
struct BiChan<T> {
port: Port<T>,
chan: Chan<T>,
}
impl<T: Send> BiChan<T> {
fn new() -> BiChan<T> {
let (port, chan) = Chan::new();
BiChan { port: port, chan: chan }
}
fn send(&self, thing: T) {
self.chan.send(thing);
}
fn recv(&self) -> T {
self.port.recv()
}
}
fn main() {
let chan = BiChan::new();
chan.send(1);
println!("{}", chan.recv());
}
Note that you can't pass these around multiple tasks because it doesn't implement Clone
, so it's pretty useless for concurrency. Implementing Clone
would be hard because we don't have a cloneable Port
at the moment – SharedPort
was removed due to unclear semantics, according to @alexcrichton.
Having a single object represent both the sending and receiving side interacts poorly with Rust's ownership rules.
@bjz Channels are bidirectional in CML because the underlying event mechanism (which provides not only send and receive events but also acknowledgements and negative acknowledgements, is generically composable ala select
, etc.) is more general and bi-directional channels are just built on top of that.
I agree with @wycats not only that having a single object for both ends would fail to take advantage of what Rust's type system is giving us.
I basically agree with @bjz here.
Source
and Sink
/Drain
have a bit of an odd effect. From the perspective of the channel the terms make sense; messages flow from the source to the sink. But from the perspective of the user, things are reversed: messages go into a source and come from a sink. I doubt that inversion is enough to trip people up though.
Agree with @anasazi that source/sink have a weird mental effect that when you try to reason about what the words mean they kind of switch back and forth - they could each be either end.
'Sender' and 'receiver' are definitely the clearest words here but they are kind of unpalatable to me for a few reasons:
1) I and others tend to rename simple variables after their type, and these words are long and 'receiver' is hard to spell. We're going to see a lot of let (sender, reciever) = Chan::open()
where now it's let (port, chan) = Chan::new()
. 'port' and 'chan' both being 4 letters is very aesthetic. Maybe this ugliness can be overcome by naming conventions ('s'/'r', 'sendr'/'recvr', 'in'/'out', etc.) or maybe I can just get used to it.
2) The stuttering problem @bjz mentioned.
3) I guess that's it...
Also I'm kind of leaning toward channel()
or chan()
over Chan::open()
. As this is such an important construct it seems fitting that it be very concise, and Chan
won't actually exist so creating a type just to make a static method seems a bit contrived.
Also, something about the nominalized (?) verbs 'sender' and 'receiver' aren't as evocative, or powerful (or something) to me as plain old nouns. Consider "I'm transmitting this message over a channel" vs. "I'm transmitting this message via a sender". This is just a vague feeling.
I think (source, sink)
better encapsulates the concept of them being a connected pair and part of a channel. This is not so obvious with (sender, receiver)
(also, receiver is a very clunky word that I will likely never learn how to spell correctly). Source and sink is only confusing if you stop thinking of them as a channel, which seems to me to be a bigger problem, but also one that the terms help to alleviate.
This was an interesting point made about source/sink on the ml:
"I am against naming it Source and Sink. It doesn't comply with the verbs we use for sending and receiving, at least for me as a non-native english speaker. While you can get/put something from/to a Source/Sink, you usually don't receive or send from a Source or Sink. The names Port and Channel are ambiguous as well, but at least you can receive (e.g. goods) from a port and send things on a channel."
This is also an uncommon use of the word 'sink' (in the real world); I've occasionally encountered confusion when using it in contexts like this.
That said, and with @glennsl's arguments, and that they prevent the stuttering problem, I'm warming up to source/sink.
I agree with the channel() -> (Sender, Receiver) proposal.
I think it's difficult to make any strong conclusions right now; first impressions aren't as important as long-term feelings with these things. Sure, source/sink seems a bit ambiguous at first glance, but thinking about the intent of a channel for a short period of time clears it up neatly. Similar could be said for sendr/recvr -- it seems awkward now, but perhaps people would warm up to it given time.
I think that the right decision will naturally arise if we give the different proposals some time to sink
in.
Then again, another serious contender could still arise. Perhaps channel isn't even the most evocative metaphor that we could come up with, if that's the name of the game (egg.lay()
and nest.hatch()
, anyone?). Or alternatively, two new words altogether could be created.
@bstrie of course has a point about bikeshedding. There are an infinite number of possibilities that we could take with this. I think, though, since channel
is so important to rust, that the names need to be carefully decided upon.
How about put
and take
?
let (put, take) = channel();
A downside to using verbs is how awkward it is for an object/struct. If the struct is a verb, you'd also have to have another verb for the method. put.put
, put.send
, are all awkward. put
and take
would really only be usable in the realm of functions.
I don't like Sender and Receiver. Sender and receiver are the tasks that use the channel, but not the objects that are used to access the channel. Perhaps you could name them ChannelInput and ChannelOutput, but I don't like these too much either.
Freestanding function versus static constructor function are both ok. Although I would be more in favor of create_channel()
and Channel::create()
because I reflects more what actually happens.
Another suggestion from the mailing list:
"Perhaps SendPort and ReceivePort (or RecvPort) would be better names?"
Likewise, InPort
and OutPort
.
I originally liked "Source" and "Sink/Drain", but I have come around to agreeing with @anasazi that, as @brson summarized, they could each be either end depending whether one takes the POV of the channel itself or not. So I've been now wondering which terms use the flow analogy but do not fail in this manner.
I happened upon these for channel output: "Tap", "Spout", "Faucet", "Spigot", all of which seem unambiguous to me, regardless of POV: both the channel itself and its client should agree that messages are going to come out of the Spout. (Though admittedly I'm using "Tap" based on its use to e.g. tap a keg, and many might instead interpret it as like a "wire tap").
But I have failed to come up with anything under this analogy that I liked for channel input.
So maybe I'm resigned to accepting "SendPort" and "RecvPort" (or "InPort" and "OutPort"). Though I still worry about any of these falling victim to the same POV problem.
As a non-native speaker I don't know any of the last words :) Inlet and outlet would be something I would at least understand.
You could also use terms from the electronical world an name them RxPort
and TxPort
. I think it like that better than ReceivePort because I could also interpret it als ReceivingPort.
Maybe thinking in terms of 'messaging' might help.
channel() -> (Port, MsgBuffer)
Canonical client code:
let (port, mbuf) = channel();
spawn(proc() {
port.send(1);
});
println!("{}", mbuf.next());
Not the most exciting names, but they might spark other, better ideas. The thing that is nice is that MsgBuffer
seems like a central location where messages are being accumulated. Once you have that type name to play off, the semantics of Port
become a great deal more clear (although it would do the opposite of what it currently does - I think)
The interesting thing is, once you accept that there is going to be a unique message buffer is for each channel, then an associated function actually begins to make a bit of sense:
let (port, buf) = MsgBuffer::new();
let local_port = port.clone();
spawn(proc() {
port.send(1);
});
local_port.send(2);
for msg in buf.iter() {
println!("{}", msg);
}
-1 to Source
and Sink
/Drain
. People arguing for it must surely see that these terms are no more intuitive than Port
and Chan
.
@bjz, I'm totally fine with a free function. After all, that's what it was for the longest time. :) HOWEVER, if we're going to embrace free functions, then let's not get in the habit of naming them after nouns. channel
isn't a verb. If what you really want is a noun, then that's what we have Foo::new
for.
While we're at it I'm not sure where arose this sentiment that Foo::new
must always return a Foo
type, with no exceptions. Rules are made to be excepted, especially in exceptional circumstances. Blind adherence to this rule would mean that we can never construct a tuple, even if it's the best fit for the situation. Chans are sufficiently exceptional (and sufficiently fundamental to Rust) that this is really a non-issue.
While we're at what we're at while we're at it, Chan::open
is a poor name for constructing an entangled pair of unidirectional endpoints. The implied analogy to files would imply accessing something that already exists. Just because it's a tuple doesn't mean it deserves to be a pariah with an arbitrarily different name for its constructor.
As for the names of the endpoints themselves, there are an infinity of potential names. If you want to exclusively optimize for clarity, you literally cannot argue against Sender
and Receiver
. If you want something that's clear but also shorter to type, maybe try Src
and Dest
(one character shorter than Port
and Chan
!). If you want ludicrously short, then just go with In
and Out
. And if you're trying to optimize for coolness, I'd suggest Transmitter
and Receiver
so that we can call bidirectional chan endpoints "transceivers". :)
+1 for transceivers
Completely agree with @bstrie.
Among his other points, open
definitely sounds like it's accessing something that's already there, whereas we're actually creating a new stream/channel/whatever-we-call-it with the constructor.
I'd be good with Transmitter
and Receiver
. (I was good with "Sender" too.)
Revisiting @brson's arguments about Sender/Receiver : tx
and rx
do seem like nice succinct variable names for a (Transmitter, Receiver)
pair that one can readily suggest in the doc for Transmitter
and Receiver
, and then tx.send(..)
/ rx.recv()
does not look so bad to me. People can even rename the types to Tx
and Rx
in a use
declaration if they really want their type names to match up that closely with their variable names.
@bstrie: if we're going to embrace free functions, then let's not get in the habit of naming them after nouns.
Agreed, that seems like a decent style-thing that could be encouraged. channel()
could be a mistake then.
@bstrie: Rules are made to be excepted, especially in exceptional circumstances.
Definitely.
Perhaps this is something we should think about a little longer? We don't have to make this change today. I still feel like we need to get our heads around the cloneable-sender-thing-unique-receiver-thing, as that will inform the direction we take. This goes beyond names.
I still feel like we need to get our heads around the cloneable-sender-thing-unique-receiver-thing, as that will inform the direction we take.
Indeed, the tragicomedy of this whole situation is that by bikeshedding at this stage we're putting the cart before the horse.
I like the current naming personally.
Fully agree with https://github.com/mozilla/rust/issues/11765#issuecomment-33225236 +1
I'm fine with Transmitter
and Receiver
I liked @brson's suggestion of InPort
and OutPort
, or some variation thereupon. "In" and "Out" are clear, concise, and allow people to avoid the cognitive overhead of i-before-e-except-after-c inherent to spelling "Receiver".
I don't mind a free function, but if we end up with multiple channel types (SharedChan, SyncChan, OnceChan, etc), it might be nice to write
let (in, out) = SharedChan::new()
The only other language I'm familiar with that has separated the send and receive side of channels is Pierce's Pict (http://www.cis.upenn.edu/~bcpierce/papers/pict/Html/Pict.html ), which used the terms input channel and output channel. So at least there's precedent for InChan/OutChan, etc.
The only other language I'm familiar with that has separated the send and receive side of channels is Pierce's Pict
Well there's also C and pipe(2)
(before you respond to that, I do not want to collude pipes and channels, and think "pipe" should refer to the Unix primitive, so if you want to argue that point please be aware I already agree with you ;)
Why does open
imply accessing something that already exists? C's fopen can create a new file, Python's open can create a new file, Haskell's openFile can create a new file. Rust's own File::create is just a thin wrapper over open_mode, which itself is just a thin wrapper over the lower-level fs_open.
channel() is OK for me, think it as a constructor function, python often use noun for example. non-verb is not a question. @bstrie @bjz
@bjz also suggested Address/Mailbox
The current
Chan::new() -> (Port, Chan)
is confusing becausenew
returns a tuple instead of a new object, 'port' and 'chan' are not intuitive, and the name 'chan' is reused for two purposes (the sender and the total channel).Some proposed alternatives:
Chan::open() -> (Source, Sink)
Chan::new() -> (Source, Sink)
Chan::new() -> (Sender, Receiver)
Chan::new() -> (Source, Drain)
pipe() -> (Port, Chan)
Also need to consider whether sender or receiver comes first in the tuple.
Nominating.
Discussion: https://mail.mozilla.org/pipermail/rust-dev/2014-January/007928.html