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.

Reply via email to