On Mon, Oct 26, 2020, 19:05 Oliver Smith <oliver.sm...@superevilmegacorp.com> wrote:
> > Hi Axel, thanks for replying; > > It isn't a pattern I teach anyone, rather it's a pattern which people I'm > encouraging to learn golang ask me about. Frequently. I was also under the > impression that generally passing a send-only channel to a function could > typically be considered an indicator the caller retains responsibility for > closing the channel. > The fact that there is no real clarity on that is exactly why I would advise against exposing channels in an API. On Sunday, October 25, 2020 at 5:18:27 PM UTC-7 axel.wa...@googlemail.com > wrote: > >> On Mon, Oct 26, 2020 at 12:29 AM Oliver Smith < >> oliver...@superevilmegacorp.com> wrote: >> >>> This pattern/idiom is very un-golike in that it's an eyesore, and one of >>> the hardest things I've found to teach people new to trying to read go code. >>> >> >> FWIW, I definitely disagree that this is somehow "un-golike" >> (non-specific, subjective terms like this have exactly this problem - no >> one really knows what it means). And I mostly disagree that it's a >> particularly bad problem - I can see where you are coming from, but at >> worst, this seems a minor issue, not worth changing the language for. >> >> I would also recommend against teaching this to newcomers to the >> language. Returning a channel in this way has a couple of issues - among >> others, it doesn't allow the caller to control buffering of the channel. >> This can be solved (which also, IMO, solves your readability issues) by >> instead *taking* a channel to write to: >> >> func filterPositiveOdds(numbers []int, ch chan<- int) { >> defer close(channel) >> for _, value := range numbers { >> if value > 0 && (value & 1) == 1 { >> channel <- value >> } >> } >> } >> >> func caller() { >> var numbers []int >> ch := make(chan int) >> go filterPositiveOdds(numbers, ch) >> for n := range ch { >> } >> } >> >> This has fundamentally the same control-flow, but we give a name to the >> function, thus making it clearer, what the extra goroutine is for. >> >> However, IMO this is still bad form. In general, I would advise exposing >> channels in APIs. It requires you to specify extra properties, like "what >> happens if the channel blocks" or "how does the operation get cancelled" in >> the documentation, without a way to get correctness compiler-checked. In >> particular, the code (both mine and yours) suffers from exactly these >> problems - if the channel is not consumed, we leak a goroutine and there is >> no way to prematurely abort consumption. Getting an error back is even >> worse. >> >> A better way is to provide a simple, synchronous iterator API like >> bufio.Scanner <https://golang.org/pkg/bufio/#Scanner> does. >> For example, you could have >> >> type IntIterator struct { >> numbers []int >> } >> >> func FilterPositiveOdds(numbers []int) *IntIterator { >> return &IntIterator{numbers} >> } >> >> func (it *IntIterator) Next() (n int, ok bool) { >> for _, n := range it.numbers { >> it.numbers = it.numbers[1:] >> if (n > 0 || n & 1 != 0) { >> return n, true >> } >> } >> return 0, false >> } >> >> In complex cases, concurrency can be hidden behind the iterator API. In >> simple cases, you could also reduce boilerplate by doing >> >> func FilterPositiveOdds(numbers []int) (next func() (n int, ok bool)) { >> return func() (n int, ok bool) { >> // same as above, but closing over numbers >> } >> } >> >> In any case - if you are unhappy with your pattern, there are many >> alternatives to choose from, within the language as it exists today. It >> seems hardly worth extra language features, to simplify this IMO rather >> uncommon construct. >> >> >> >>> I've toyed with a few alternatives, but I'm having a hard time finding >>> something I think everyone would like. >>> >>> Pattern the first; 'go return` which flips the idiom with syntactic >>> sugar. 'go return' returns the function and continues the remainder of it >>> in a go func() { ... } >>> >>> ```go >>> func getPositiveOdds(numbers []int) <-chan int { >>> channel := make(chan int) >>> go return channel >>> >>> defer close(channel) >>> for _, value := range numbers { >>> if value > 0 && (value & 1) == 1 { >>> channel <- value >>> } >>> } >>> } >>> ``` >>> >>> The second more closely binds to the channel-generator pattern by >>> extending make(chan) to take a 'go func' parameter: >>> >>> ```go >>> func getPositiveOdds(numbers []int) <-chan int { >>> generator := make(chan int, go func (channel chan int) { >>> defer close(channel) >>> for _, value := range numbers { >>> if value > 0 && (value & 1) == 1 { >>> channel <- value >>> } >>> } >>> }) >>> return generator >>> } >>> ``` >>> >>> I was tempted by the notion of having this auto-defer-close the channel, >>> but I think retaining the explicit close is generally better and more >>> teachable. >>> >>> Where it might be tenable would be using syntactic sugar to perhaps >>> better promote the notion of generators toward 1st class citizens: >>> >>> ```go >>> >>> // 'generator' is syntactic sugar, and instead of a return type you provide >>> // the make statement to create the required channel type. >>> generator getPositiveOdds(numbers []int) make(chan int, 1) { >>> >>> // The variable 'yield' is automatically defined as the channel created >>> for _, value := range numbers { >>> if value > 0 && (value & 1) == 1 { >>> yield <- value >>> } >>> } >>> } >>> ``` >>> >>> which is roughly equivalent to: >>> >>> ```golang >>> func getPositiveOdds(numbers []int]) <-chan int { >>> generator := func (yield chan int) { >>> numbers := numbers >>> for _, value := range numbers { >>> if value > 0 && (value & 1) == 1 { >>> yield <- value >>> } >>> } >>> } >>> >>> channel := make(chan int, 1) >>> >>> go func () { >>> defer close(channel) >>> generator(channel) >>> }() >>> >>> return channel >>> } >>> ``` >>> >>> `gen` instead of `generator` might be ok, I just wanted to be >>> unambiguous. >>> >>> -- >>> You received this message because you are subscribed to the Google >>> Groups "golang-nuts" group. >>> To unsubscribe from this group and stop receiving emails from it, send >>> an email to golang-nuts...@googlegroups.com. >>> To view this discussion on the web visit >>> https://groups.google.com/d/msgid/golang-nuts/5aa92ac7-1c9b-48af-8389-a6870563b831n%40googlegroups.com >>> <https://groups.google.com/d/msgid/golang-nuts/5aa92ac7-1c9b-48af-8389-a6870563b831n%40googlegroups.com?utm_medium=email&utm_source=footer> >>> . >>> >> -- > You received this message because you are subscribed to the Google Groups > "golang-nuts" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to golang-nuts+unsubscr...@googlegroups.com. > To view this discussion on the web visit > https://groups.google.com/d/msgid/golang-nuts/383cbb48-5bda-426a-805a-8d3efa82d6e8n%40googlegroups.com > <https://groups.google.com/d/msgid/golang-nuts/383cbb48-5bda-426a-805a-8d3efa82d6e8n%40googlegroups.com?utm_medium=email&utm_source=footer> > . > -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAEkBMfFBB%3DAOy8F%2B94zkraTUsU5V5%3DXe0LMB5C%2BdYMHEL947Sg%40mail.gmail.com.