Skip to content

Rename channel types and constructor #11765

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

Closed
brson opened this issue Jan 24, 2014 · 84 comments · Fixed by #12815
Closed

Rename channel types and constructor #11765

brson opened this issue Jan 24, 2014 · 84 comments · Fixed by #12815
Milestone

Comments

@brson
Copy link
Contributor

brson commented Jan 24, 2014

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:

  • 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

@tarcieri
Copy link
Contributor

I suggest:

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

@wycats
Copy link
Contributor

wycats commented Jan 24, 2014

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

@wycats
Copy link
Contributor

wycats commented Jan 24, 2014

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
Copy link
Contributor

dherman commented Jan 24, 2014

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

@brendanzab
Copy link
Member

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
Copy link
Contributor

wycats commented Jan 24, 2014

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

let (send, receive) = channel();
send(x);
let y = receive();

@tikue
Copy link
Contributor

tikue commented Jan 24, 2014

My thoughts:

  • I think libstd should try hard conform to 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:
  • I also think @bjz makes a pretty good point that there's nothing wrong with standalone functions.
  • For something so visible in Rust, it'd be nice to have a more personal touch than (Sender, Receiver).

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

@wycats
Copy link
Contributor

wycats commented Jan 24, 2014

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
Copy link
Member

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
Copy link
Contributor

I like channel()

@wycats
Copy link
Contributor

wycats commented Jan 24, 2014

@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
Copy link
Contributor

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

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

@wycats
Copy link
Contributor

wycats commented Jan 24, 2014

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

@brendanzab
Copy link
Member

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
Copy link
Member

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
Copy link
Contributor

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
Copy link
Member

@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
Copy link
Member

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
Copy link
Contributor

wycats commented Jan 24, 2014

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

@larsbergstrom
Copy link
Contributor

@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
Copy link
Contributor

anasazi commented Jan 24, 2014

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
Copy link
Contributor Author

brson commented Jan 24, 2014

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
Copy link
Contributor Author

brson commented Jan 24, 2014

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
Copy link
Contributor Author

brson commented Jan 24, 2014

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
Copy link

glennsl commented Jan 24, 2014

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
Copy link
Contributor Author

brson commented Jan 24, 2014

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
Copy link
Contributor Author

brson commented Jan 24, 2014

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

@gsemet
Copy link

gsemet commented Jan 24, 2014

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

@tikue
Copy link
Contributor

tikue commented Jan 24, 2014

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
Copy link
Contributor

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.

@liigo
Copy link
Contributor

liigo commented Feb 23, 2014

2014年2月23日 下午3:35于 "Alex Crichton" [email protected]写道:

Another idea:

fn channel() -> (SendPort, RecvPort)

Sadly SyncSendPort is awful. I do like the idea that each half is a
"port" to the whole concept of a channel.

+1, and +1 for syncport.


Reply to this email directly or view it on GitHub.

@Valloric
Copy link
Contributor

I do like the idea that each half is a "port" to the whole concept of a channel.

While I agree that "port" is a nice way to describe an end of a channel, I do not think that having it in the name we provide to the user is useful; as you noticed, we'd still have to attach another word (like "Send" or "Recv") to make it meaningful. And "Recv" being shortened kinda gives it away that "Port" is useless in the name and doesn't pull its own weight but merely adds to the letters the user has to type out.

"Source" and "Sink" are short and unambiguous. Conceptually, they're ports but making them "SourcePort" and "SinkPort" is just adding letters for no benefit.

"Sender" and "Receiver" are also fine (though longer than "Source" and "Sink" for IMO no increase in readability); I'd strongly encourage avoiding adding more butchered shorthands like "Recv" to the standard library. Frankly, Rust already suffers for it.

@lilyball
Copy link
Contributor

"Sink" is only unambiguous once you explain what the heck it actually means.

@tarcieri
Copy link
Contributor

Given the traditional role of "port" in Berkeley sockets, I'd consider naming the endpoints of a channel "ports" is confusing.

@Valloric
Copy link
Contributor

"Sink" is only unambiguous once you explain what the heck it actually means.

On it's own, it's hard to understand what it is. But the name is not presented to the user in a vacuum. The name is presented in the context of "A Channel has a Source and a Sink endpoint." In that one sentence, it's immediately obvious to the user (now and forever) what end does what.

@brendanzab
Copy link
Member

"A Channel has a Source and a Sink endpoint." In that one sentence, it's immediately obvious to the user (now and forever) what end does what.

Remember the POV issue. Does a Source call send or recv? It is not very clear.

@Valloric
Copy link
Contributor

Remember the POV issue. Does a Source call send or recv? It is not very clear.

How is it an issue? The user has a Source and a Sink in their code. Per English, stuff comes out of a Source and goes into a Sink. Thus, you can get data out of the Channel from the Source and put it into the Channel through the Sink.

@brendanzab
Copy link
Member

Is Sink a handle to something you send to, or is it the end point that receive from?

@tarcieri
Copy link
Contributor

+1 for source/sink (or upstream/downstream) being confusing

@brendanzab
Copy link
Member

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

@pnkfelix #11765 (comment)

The advantage of Sender and Receiver is that the method names are actually in the type names. That makes it easy to remember.

@Valloric
Copy link
Contributor

I disagree about the POV issue, but since I think Sender and Receiver are fine names I don't think this is worth discussing further.

@alexcrichton
Copy link
Member

I'm not super thrilled about Sender/Receiver, but I think that clarity is more important than conciceness. To reiterate @brson's comment (#11765 (comment)), this ticket needs these changes:

  • Rename Chan<T> to Sender<T>
  • Rename Port<T> to Receiver<T>
  • Rename Chan::new to channel (and put channel in the prelude)
  • Rename variables/fields for the old Chan type to a derivative tx
  • Rename variables/fields for the old Port type to a derivative rx

The only outstanding decision I can think of are what PortReader and ChanWriter should become, and I'd recommend ChanWriter and ChanReader now that we have the design space open up (these types are readers/writers of channels).

@lilyball
Copy link
Contributor

One concern that occurred to me tonight is that Sender and Receiver are -er words, which tend to imply traits (e.g. Writer, Reader), but in this case are concrete types. I know we aren't terribly consistent about this, but the -er concrete types that I can think of off the top of my head are named after the traits they implement, e.g. MemWriter.

bors added a commit that referenced this issue Mar 13, 2014
* Chan<T> => Sender<T>
* Port<T> => Receiver<T>
* Chan::new() => channel()
* constructor returns (Sender, Receiver) instead of (Receiver, Sender)
* local variables named `port` renamed to `rx`
* local variables named `chan` renamed to `tx`

Closes #11765
flip1995 pushed a commit to flip1995/rust that referenced this issue Nov 16, 2023
`needless_return_with_question_mark` ignore let-else

Fixes rust-lang#11765

This PR makes `needless_return_with_question_mark` to ignore expr inside let-else.

changelog: [`needless_return_with_question_mark`] ignore let-else
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.