rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.63k stars 12.63k forks source link

Rename channel types and constructor #11765

Closed brson closed 10 years ago

brson commented 10 years ago

The current Chan::new() -> (Port, Chan) is confusing because new 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:

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

tarcieri commented 10 years ago

I suggest:

Chan::open() -> (Sender, Receiver)

wycats commented 10 years ago

I agree with with @bascule. let (sender, receiver) = Chan::open() feels natural to me.

wycats commented 10 years ago

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.

dherman commented 10 years ago

Huzzah for this bug! Boo for paired terminology without mnemonics.

brendanzab commented 10 years ago

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();
wycats commented 10 years ago

@bjz it's too bad there's no Call trait (yet?).

let (send, receive) = channel();
send(x);
let y = receive();
tikue commented 10 years ago

My thoughts:

Out of these options, my vote goes to channel() -> (Source, Sink) or channel() -> (Source, Drain).

wycats commented 10 years ago

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.

brendanzab commented 10 years ago

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.

tarcieri commented 10 years ago

I like channel()

wycats commented 10 years ago

@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.

tarcieri commented 10 years ago

I think "channel" is definitely the correct term due to its heritage in CSP:

https://en.wikipedia.org/wiki/Channel_(programming)

wycats commented 10 years ago

I'm also ok with channel() or Channel::open().

brendanzab commented 10 years ago

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.

brendanzab commented 10 years ago

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 the out_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;
thehydroimpulse commented 10 years ago

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.

brendanzab commented 10 years ago

@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 SyncChan in extra::comm. The issue with that is they can't be cloned easily now that SharedPort was removed. Woops SyncChan is directional too (facepalm). I do think it would be easy to create one from our current primitives though.

brendanzab commented 10 years ago
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.

wycats commented 10 years ago

Having a single object represent both the sending and receiving side interacts poorly with Rust's ownership rules.

larsbergstrom commented 10 years ago

@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.

anasazi commented 10 years ago

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.

brson commented 10 years ago

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...

brson commented 10 years ago

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.

brson commented 10 years ago

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.

glennsl commented 10 years ago

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.

brson commented 10 years ago

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.

brson commented 10 years ago

That said, and with @glennsl's arguments, and that they prevent the stuttering problem, I'm warming up to source/sink.

gsemet commented 10 years ago

I agree with the channel() -> (Sender, Receiver) proposal.

tikue commented 10 years ago

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.

thehydroimpulse commented 10 years ago

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.

Matthias247 commented 10 years ago

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.

brson commented 10 years ago

Another suggestion from the mailing list:

"Perhaps SendPort and ReceivePort (or RecvPort) would be better names?"

Likewise, InPort and OutPort.

pnkfelix commented 10 years ago

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.

Matthias247 commented 10 years ago

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.

brendanzab commented 10 years ago

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)

brendanzab commented 10 years ago

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);
}
bstrie commented 10 years ago

Hey You Asked For Bikeshedding

--OR--

Why Everyone Is Wrong Forever

-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". :)

emberian commented 10 years ago

+1 for transceivers

huonw commented 10 years ago

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.

pnkfelix commented 10 years ago

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.

brendanzab commented 10 years ago

@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.

bstrie commented 10 years ago

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.

adrientetar commented 10 years ago

I like the current naming personally.

flaper87 commented 10 years ago

Fully agree with https://github.com/mozilla/rust/issues/11765#issuecomment-33225236 +1

I'm fine with Transmitter and Receiver

zslayton commented 10 years ago

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()

larsbergstrom commented 10 years ago

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.

tarcieri commented 10 years ago

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 ;)

jfager commented 10 years ago

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.

liigo commented 10 years ago

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

brson commented 10 years ago

@bjz also suggested Address/Mailbox