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

Proposal: Remove ability to specify directions in channel types #23415

Closed
griesemer opened this Issue Jan 11, 2018 · 21 comments

Comments

Projects
None yet
@griesemer
Contributor

griesemer commented Jan 11, 2018

Direction specifiers on channels are a form of type constraint that seems rarely used (TODO: provide data). While it may be useful documentation, in reality it is hard to enforce: If a channel is marked as a read- or write-only channel, it may still be possible to send or receive from it through an unrestricted alias that refers to the same channel (which cannot easily be prevented statically at compile time, in general). A channel directions is a type constraint, not a runtime property.

Creating a directed channel with make doesn't make much sense either: It could only be used to send or receive from it which would render it virtually useless as it cannot be assigned to an unrestricted channel variable. (One could use an unsafe type conversion, but that's not the point.)

Thus, besides documentation, directed channel types serve mainly as a restriction on functions operating on them. But it's unclear that the restriction prevents a significant source of bugs: A function that sends rather than receives (or vice versa) from a channel is likely to cause immediate trouble (deadlock) upon its first wrong channel operation; such a function is unlikely to survive a basic test case.

Specifying a channel direction may be most useful in package interfaces, but there is a growing consensus that generally channels shouldn't be passed across significant API boundaries.

Channel directions are unusual in the Go type system in the sense that there are no similar constraints on other types: Go doesn't have have a read-only or write-only attribute on any other type or variable; which again may be very useful across API boundaries. Instead, we cope with providing accessors (functions/methods) instead, to prevent undesired reads or writes. One could use the same mechanism for channels.

Finally (and perhaps least importantly), the syntax for channel directions is a source of ambiguity when parsing conversion expressions (https://tip.golang.org/ref/spec#Conversions) which requires incommensurable extra work during parsing.

In summary, I propose that we remove the ability to specify channel directions for Go 2.

This would not be a backward-compatible change. In a first step, one might simply ignore the direction specification. In a second step, one might remove the ability to specify the direction altogether. If we decide on a mechanism (annotation in source) for selecting the accepted language at compile time, we could change the syntax more readily.

Finally, there's the issue of reflection: The reflection API allows to inspect a channel direction, and to create directed channel types. For Go 2 channels, that direction would always be unrestricted. For backward compatibility, reflect may need to continue supporting directions. Perhaps the restrictions can me made no-ops.

@gopherbot gopherbot added this to the Proposal milestone Jan 11, 2018

@gopherbot gopherbot added the Proposal label Jan 11, 2018

@griesemer griesemer added Go2 and removed Proposal labels Jan 11, 2018

@gopherbot gopherbot added the Proposal label Jan 11, 2018

@bradfitz

This comment has been minimized.

Show comment
Hide comment
@bradfitz

bradfitz Jan 11, 2018

Member

I know I'm in the minority amongst the Go team on this one, but I really like type restrictions. I agree that it's weird that channels can be restricted and nothing else can, but my personal preference is to let more things be restricted instead. Again, I know y'all hate that.

But I would be sad to see this enforced documentation go away.

Member

bradfitz commented Jan 11, 2018

I know I'm in the minority amongst the Go team on this one, but I really like type restrictions. I agree that it's weird that channels can be restricted and nothing else can, but my personal preference is to let more things be restricted instead. Again, I know y'all hate that.

But I would be sad to see this enforced documentation go away.

@jimmyfrasche

This comment has been minimized.

Show comment
Hide comment
@jimmyfrasche

jimmyfrasche Jan 11, 2018

Member

Channel directions are unusual in the Go type system in the sense that there are no similar constraints on other types: Go doesn't have have a read-only or write-only attribute on any other type or variable; which again may be very useful across API boundaries. Instead, we cope with providing accessors (functions/methods) instead, to prevent undesired reads or writes. One could use the same mechanism for channels.

You could't use accessors with select unless it returned the channel which defeats the purpose

Member

jimmyfrasche commented Jan 11, 2018

Channel directions are unusual in the Go type system in the sense that there are no similar constraints on other types: Go doesn't have have a read-only or write-only attribute on any other type or variable; which again may be very useful across API boundaries. Instead, we cope with providing accessors (functions/methods) instead, to prevent undesired reads or writes. One could use the same mechanism for channels.

You could't use accessors with select unless it returned the channel which defeats the purpose

@as

This comment has been minimized.

Show comment
Hide comment
@as

as Jan 11, 2018

Contributor

It would be very unfortunate to write wrapper methods for every API that returns a read-only channel. I don't really care about the performance difference of having 50,000 goroutines calling foo.Next() *Item vs foo.Chan() <-chan *Item once, but not having the ability to select on foo.Next() *Item just kills a major advantage of Go for a lot of pub/sub systems currently in production.

Contributor

as commented Jan 11, 2018

It would be very unfortunate to write wrapper methods for every API that returns a read-only channel. I don't really care about the performance difference of having 50,000 goroutines calling foo.Next() *Item vs foo.Chan() <-chan *Item once, but not having the ability to select on foo.Next() *Item just kills a major advantage of Go for a lot of pub/sub systems currently in production.

@robpike

This comment has been minimized.

Show comment
Hide comment
@robpike

robpike Jan 11, 2018

Contributor

In the chain of concurrent language designs that led to Go, this was the missing piece. Go is the first language in the sequence that has them, and it was an important piece of the puzzle. Like @bradfitz, I would be very sad to see them disappear.

The fact that they are used rarely is not a design problem but an education one.

Also, I disagree with the blanket disdain for channels crossing API boundaries. Not every program is a web server

In any case, returning a channel from a function is a common pattern even if returning one from a package is not. Being able to express the channel direction in a function call or return adds type safety. Why remove that ability?

Contributor

robpike commented Jan 11, 2018

In the chain of concurrent language designs that led to Go, this was the missing piece. Go is the first language in the sequence that has them, and it was an important piece of the puzzle. Like @bradfitz, I would be very sad to see them disappear.

The fact that they are used rarely is not a design problem but an education one.

Also, I disagree with the blanket disdain for channels crossing API boundaries. Not every program is a web server

In any case, returning a channel from a function is a common pattern even if returning one from a package is not. Being able to express the channel direction in a function call or return adds type safety. Why remove that ability?

@griesemer

This comment has been minimized.

Show comment
Hide comment
@griesemer

griesemer Jan 11, 2018

Contributor

@jimmyfrasche , @as Of course, we wouldn't want to write wrapper methods for all read-only, write-only channels; and I'm aware that select would be a problem (for accessors). Similarly, we wouldn't want to write accessors for all variables if the the language had a mechanism to specify a read-only attribute for any variable and now it were proposed that it be removed.

The point I am trying to make is that while channel directions may be useful annotations, they don't carry their weight in extra complexity. There is quite a bit of extra prose required to explain channel directions, code required to parse them correctly (due to the ambiguity), and a little bit to handle them during type-checking. Yet after that, they have zero semantic effect, except that the information has to be carried along into reflection info. Just because it is useful, doesn't mean it's important to have.

@robpike I am unsure as to which part of the puzzle you are referring to; but I am probably forgetting something.

As we are thinking about changes for Go 2, I do see a lot of additions but little in the way of cleaning up what we have. In my mind this is a place where we can simplify and make the language more regular in a meaningful way with virtually zero impact on its power and usability.

Let's make no mistake: If we are not willing to streamline and remove things, we are committing to a path of steady additions to the language. Ultimately this is the fate to which most other languages succumbed to. (There may be better places to simplify, and this is a proposal only.)

Thus, I would like to see a concrete argument as to why being able to specify the direction of a channel is crucially important. And if it is, shouldn't there be a similar constraint (read/write access) for any other type? (Or perhaps a general mechanism that unifies and thus simplifies?).

Contributor

griesemer commented Jan 11, 2018

@jimmyfrasche , @as Of course, we wouldn't want to write wrapper methods for all read-only, write-only channels; and I'm aware that select would be a problem (for accessors). Similarly, we wouldn't want to write accessors for all variables if the the language had a mechanism to specify a read-only attribute for any variable and now it were proposed that it be removed.

The point I am trying to make is that while channel directions may be useful annotations, they don't carry their weight in extra complexity. There is quite a bit of extra prose required to explain channel directions, code required to parse them correctly (due to the ambiguity), and a little bit to handle them during type-checking. Yet after that, they have zero semantic effect, except that the information has to be carried along into reflection info. Just because it is useful, doesn't mean it's important to have.

@robpike I am unsure as to which part of the puzzle you are referring to; but I am probably forgetting something.

As we are thinking about changes for Go 2, I do see a lot of additions but little in the way of cleaning up what we have. In my mind this is a place where we can simplify and make the language more regular in a meaningful way with virtually zero impact on its power and usability.

Let's make no mistake: If we are not willing to streamline and remove things, we are committing to a path of steady additions to the language. Ultimately this is the fate to which most other languages succumbed to. (There may be better places to simplify, and this is a proposal only.)

Thus, I would like to see a concrete argument as to why being able to specify the direction of a channel is crucially important. And if it is, shouldn't there be a similar constraint (read/write access) for any other type? (Or perhaps a general mechanism that unifies and thus simplifies?).

@bradfitz

This comment has been minimized.

Show comment
Hide comment
@bradfitz

bradfitz Jan 11, 2018

Member

@griesemer, one possible compromise is that channel directions could exist only syntactically as documentation but not be part of the type system or reflect info, and leave it up to vet (or cmd/compile, maybe) to check violations of.

It'd be somewhat akin to struct tags which are in the language only barely but mostly enforced outside of the language.

Member

bradfitz commented Jan 11, 2018

@griesemer, one possible compromise is that channel directions could exist only syntactically as documentation but not be part of the type system or reflect info, and leave it up to vet (or cmd/compile, maybe) to check violations of.

It'd be somewhat akin to struct tags which are in the language only barely but mostly enforced outside of the language.

@faiface

This comment has been minimized.

Show comment
Hide comment
@faiface

faiface Jan 11, 2018

seems rarely used

This is weird. When I write concurrent Go code, pretty much every channel variable has a direction constraint.

And that makes sense. There is not a lot of situations (if there are even any) when one goroutine needs to send and receive on the same channel. It seems to me that in pretty much every sound concurrent system, one goroutine either sends or receives on one channel, but never both.

Considering this, direction constraint on channels makes huge sense, as it should be used almost always and makes it clear where a goroutine is located in the net of channels.

faiface commented Jan 11, 2018

seems rarely used

This is weird. When I write concurrent Go code, pretty much every channel variable has a direction constraint.

And that makes sense. There is not a lot of situations (if there are even any) when one goroutine needs to send and receive on the same channel. It seems to me that in pretty much every sound concurrent system, one goroutine either sends or receives on one channel, but never both.

Considering this, direction constraint on channels makes huge sense, as it should be used almost always and makes it clear where a goroutine is located in the net of channels.

@as

This comment has been minimized.

Show comment
Hide comment
@as

as Jan 11, 2018

Contributor

Its easier to explain a compile time error than debug an improper use of a channel meant to be read-only. In my experience, Go is easy to work with because it takes away the option to do the wrong thing. New users from other languages, specifically from ones with naive concurrency primitives, do the wrong thing.

Go doesn't exist in a vacuum of open source projects. I can count several production systems that return receive only channels and ensure that new programmers do not do crazy things with them. The guarantee that a package importer can't interrupt a message pump by closing or writing to a channel is very nice.

Contributor

as commented Jan 11, 2018

Its easier to explain a compile time error than debug an improper use of a channel meant to be read-only. In my experience, Go is easy to work with because it takes away the option to do the wrong thing. New users from other languages, specifically from ones with naive concurrency primitives, do the wrong thing.

Go doesn't exist in a vacuum of open source projects. I can count several production systems that return receive only channels and ensure that new programmers do not do crazy things with them. The guarantee that a package importer can't interrupt a message pump by closing or writing to a channel is very nice.

@pciet

This comment has been minimized.

Show comment
Hide comment
@pciet

pciet Jan 11, 2018

Contributor

Maybe the annotation will become important for me in future concurrent programs, but from my experience I’d be ok with removing channel direction annotation.

Function naming documents expected channel uses, the channel send and receive syntax is already resilient to easy mistakes, figuring out the syntax takes more effort than most constructs, and channel direction seems easy to reason about.

Contributor

pciet commented Jan 11, 2018

Maybe the annotation will become important for me in future concurrent programs, but from my experience I’d be ok with removing channel direction annotation.

Function naming documents expected channel uses, the channel send and receive syntax is already resilient to easy mistakes, figuring out the syntax takes more effort than most constructs, and channel direction seems easy to reason about.

@peterbourgon

This comment has been minimized.

Show comment
Hide comment
@peterbourgon

peterbourgon Jan 16, 2018

Member

Direction specifiers on channels are a form of type constraint that seems rarely used

I also must disagree here. Direction constraints are an important bit of context that significantly aid code comprehension. I use them at every opportunity, and I would be flabbergasted to see this valuable feature removed. I echo @bradfitz and wish constraints like this were applicable to more types, not fewer (e.g. const).

Member

peterbourgon commented Jan 16, 2018

Direction specifiers on channels are a form of type constraint that seems rarely used

I also must disagree here. Direction constraints are an important bit of context that significantly aid code comprehension. I use them at every opportunity, and I would be flabbergasted to see this valuable feature removed. I echo @bradfitz and wish constraints like this were applicable to more types, not fewer (e.g. const).

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Jan 16, 2018

I'm also one of the people who use channel directions whenever a channel is passed outside a single function. And it is a thing I learned from stepping into that particular piece of lego one too many times. Yes, the deadlocks were caught by simple testing, but they did take a while to figure out and if I had used channel directions from the get-go, they would've been more obvious and flaws in the model would have been immediately clear.

Personally, I'd much rather remove undirected channel types, than directed ones.

Creating a directed channel with make doesn't make much sense either: It could only be used to send or receive from it which would render it virtually useless as it cannot be assigned to an unrestricted channel variable.

Alternatively, make could return two channels, one for reading and one for writing. If we want a backwards-compatible change, we could make that an alternative form (akin to ,ok) or use a separate identifier, if we are unhappy about the continual overloading of make (or think this doesn't fit the bill of calling something make).

Merovius commented Jan 16, 2018

I'm also one of the people who use channel directions whenever a channel is passed outside a single function. And it is a thing I learned from stepping into that particular piece of lego one too many times. Yes, the deadlocks were caught by simple testing, but they did take a while to figure out and if I had used channel directions from the get-go, they would've been more obvious and flaws in the model would have been immediately clear.

Personally, I'd much rather remove undirected channel types, than directed ones.

Creating a directed channel with make doesn't make much sense either: It could only be used to send or receive from it which would render it virtually useless as it cannot be assigned to an unrestricted channel variable.

Alternatively, make could return two channels, one for reading and one for writing. If we want a backwards-compatible change, we could make that an alternative form (akin to ,ok) or use a separate identifier, if we are unhappy about the continual overloading of make (or think this doesn't fit the bill of calling something make).

@christophberger

This comment has been minimized.

Show comment
Hide comment
@christophberger

christophberger Jan 16, 2018

Channel directions are unusual in the Go type system in the sense that there are no similar constraints on other types: Go doesn't have have a read-only or write-only attribute on any other type or variable

Unlike other types (that are "just" data containers), channels do have an inherent direction property, as they are designed for transporting values from a sender to a receiver. Sending data the wrong direction is a logical error, and I'd rather have the error caught for me at compile time than during tests that have to be written to take effect.

(Edited to add:) I do agree on the "hard to enforce" part.

christophberger commented Jan 16, 2018

Channel directions are unusual in the Go type system in the sense that there are no similar constraints on other types: Go doesn't have have a read-only or write-only attribute on any other type or variable

Unlike other types (that are "just" data containers), channels do have an inherent direction property, as they are designed for transporting values from a sender to a receiver. Sending data the wrong direction is a logical error, and I'd rather have the error caught for me at compile time than during tests that have to be written to take effect.

(Edited to add:) I do agree on the "hard to enforce" part.

@lpar

This comment has been minimized.

Show comment
Hide comment
@lpar

lpar Jan 16, 2018

I didn't even know you could have bidirectional channels, that's how often I've wanted them.

lpar commented Jan 16, 2018

I didn't even know you could have bidirectional channels, that's how often I've wanted them.

@j7b

This comment has been minimized.

Show comment
Hide comment
@j7b

j7b Jan 16, 2018

I feel the 3d, 4th and 5th paragraphs are more arguments for a "no publicly visible channels" proposal, which I'd personally not propound but appreciate the merits of.

The first paragraph bothers me in the same way I was bothered by the proposal to remove the complex types; how many lines of public user code are required to justify something's existence?

As make-ing directed channels has limited utility I'd suggest prohibiting same in Go2, but it's disconcerting to think of what could happen if the direction "documentation" wasn't enforced if channels are allowed in public.

j7b commented Jan 16, 2018

I feel the 3d, 4th and 5th paragraphs are more arguments for a "no publicly visible channels" proposal, which I'd personally not propound but appreciate the merits of.

The first paragraph bothers me in the same way I was bothered by the proposal to remove the complex types; how many lines of public user code are required to justify something's existence?

As make-ing directed channels has limited utility I'd suggest prohibiting same in Go2, but it's disconcerting to think of what could happen if the direction "documentation" wasn't enforced if channels are allowed in public.

@ccbrown

This comment has been minimized.

Show comment
Hide comment
@ccbrown

ccbrown Jan 18, 2018

I like the feature, but I also think it's one extremely few people would have missed if it were never introduced to begin with. I would be a fan of removing it if it helps to offset the language complexity of the higher demand features I hope to see in Go 2. You have my 👍 for that reason.

ccbrown commented Jan 18, 2018

I like the feature, but I also think it's one extremely few people would have missed if it were never introduced to begin with. I would be a fan of removing it if it helps to offset the language complexity of the higher demand features I hope to see in Go 2. You have my 👍 for that reason.

@object88

This comment has been minimized.

Show comment
Hide comment
@object88

object88 Jan 29, 2018

I can't speak to language tooling complexity, but channel directionality made immediate sense to me, and I decorate my channels at every opportunity to reflect their intended usage, esp. in the context of the adage, Do not communicate by sharing memory; instead, share memory by communicating.

object88 commented Jan 29, 2018

I can't speak to language tooling complexity, but channel directionality made immediate sense to me, and I decorate my channels at every opportunity to reflect their intended usage, esp. in the context of the adage, Do not communicate by sharing memory; instead, share memory by communicating.

@andlabs

This comment has been minimized.

Show comment
Hide comment
@andlabs

andlabs Jan 31, 2018

Contributor

Wait, so is

c := make(chan<- int)

currently legal? If so, why? Directionality constraints are strictly for the purpose of restricting what a consumer of an API can do with a channel; I would imagine the creator of the channel still wants to be able to use the channel unconstrained. If anything, we should at least get rid of the ability to make directioned channels in calls to make.

And I don't understand the arguments against exposing channels. Why is that a bad thing anyway, and why particularly for web servers?

Contributor

andlabs commented Jan 31, 2018

Wait, so is

c := make(chan<- int)

currently legal? If so, why? Directionality constraints are strictly for the purpose of restricting what a consumer of an API can do with a channel; I would imagine the creator of the channel still wants to be able to use the channel unconstrained. If anything, we should at least get rid of the ability to make directioned channels in calls to make.

And I don't understand the arguments against exposing channels. Why is that a bad thing anyway, and why particularly for web servers?

@griesemer

This comment has been minimized.

Show comment
Hide comment
@griesemer

griesemer Jan 31, 2018

Contributor

@andlabs One could actually use a send-only channel, created via make(chan<- T) (for some suitable T) without ever needing to read from it, as this slightly convoluted example shows: https://play.golang.org/p/nAa9q4AsUX6 .

Contributor

griesemer commented Jan 31, 2018

@andlabs One could actually use a send-only channel, created via make(chan<- T) (for some suitable T) without ever needing to read from it, as this slightly convoluted example shows: https://play.golang.org/p/nAa9q4AsUX6 .

@go101

This comment has been minimized.

Show comment
Hide comment
@go101

go101 Feb 1, 2018

@andlabs
send-only channel can be used as a counter by calling len(aSendOnlyChannel), though the counter has a ceiling limit.

go101 commented Feb 1, 2018

@andlabs
send-only channel can be used as a counter by calling len(aSendOnlyChannel), though the counter has a ceiling limit.

@griesemer

This comment has been minimized.

Show comment
Hide comment
@griesemer

griesemer Apr 17, 2018

Contributor

I'm going to retract this proposal; clearly there's no support for this idea.

Contributor

griesemer commented Apr 17, 2018

I'm going to retract this proposal; clearly there's no support for this idea.

@lpar

This comment has been minimized.

Show comment
Hide comment
@lpar

lpar Aug 20, 2018

Having checked the original CSP paper, I find this in the proposals:

(4) Such communication occurs when one process names another as destination for output and the second process names the first as source for input. In this case, the value to be output is copied from the first process to the second. There is no automatic buffeting: In general, an input or output command is delayed until the other process is ready with the corresponding output or input. Such delay is invisible to the delayed process.

I also checked "Communicating Sequential Processes" (the book) by C.A.R. Hoare, author of the original CSP proposal. Chapter 4, which introduces channels, states:

We shall observe the convention that channels are used for communication in only one direction and between only two processes.

So it looks to me as if it was only ever intended for CSP channels to be unidirectional. Which doesn't in itself mean Go shouldn't have bidirectional channels, of course, but would probably explain why people with a CS background find directional channels natural.

Also of note, Rust channels are deliberately unidirectional. I'd also guess that CSP correctness verification is broken by bidirectional channels, but I have no proof to hand.

lpar commented Aug 20, 2018

Having checked the original CSP paper, I find this in the proposals:

(4) Such communication occurs when one process names another as destination for output and the second process names the first as source for input. In this case, the value to be output is copied from the first process to the second. There is no automatic buffeting: In general, an input or output command is delayed until the other process is ready with the corresponding output or input. Such delay is invisible to the delayed process.

I also checked "Communicating Sequential Processes" (the book) by C.A.R. Hoare, author of the original CSP proposal. Chapter 4, which introduces channels, states:

We shall observe the convention that channels are used for communication in only one direction and between only two processes.

So it looks to me as if it was only ever intended for CSP channels to be unidirectional. Which doesn't in itself mean Go shouldn't have bidirectional channels, of course, but would probably explain why people with a CS background find directional channels natural.

Also of note, Rust channels are deliberately unidirectional. I'd also guess that CSP correctness verification is broken by bidirectional channels, but I have no proof to hand.

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