Подтвердить что ты не робот

Идиоматический способ реализации генераторов (выход) в Голанге для рекурсивных функций

[Примечание: я читаю генераторы стиля Python в Go, это не дубликат. ]

В Python/Ruby/JavaScript/ECMAScript 6 функции генератора могут быть записаны с использованием ключевого слова yield, предоставленного языком. В Go он может быть смоделирован с использованием goroutine и канала.

Код

Следующий код показывает, как можно реализовать функцию перестановки (abcd, abdc, acbd, acdb,..., dcba):

// $src/lib/lib.go

package lib

// private, starts with lowercase "p"
func permutateWithChannel(channel chan<- []string, strings, prefix []string) {
    length := len(strings)
    if length == 0 {
        // Base case
        channel <- prefix
        return
    }
    // Recursive case
    newStrings := make([]string, 0, length-1)
    for i, s := range strings {
        // Remove strings[i] and assign the result to newStringI
        // Append strings[i] to newPrefixI
        // Call the recursive case
        newStringsI := append(newStrings, strings[:i]...)
        newStringsI = append(newStringsI, strings[i+1:]...)
        newPrefixI := append(prefix, s)
        permutateWithChannel(channel, newStringsI, newPrefixI)
    }
}

// public, starts with uppercase "P"
func PermutateWithChannel(strings []string) chan []string {
    channel := make(chan []string)
    prefix := make([]string, 0, len(strings))
    go func() {
        permutateWithChannel(channel, strings, prefix)
        close(channel)
    }()
    return channel
}

Вот как это можно использовать:

// $src/main.go

package main

import (
    "./lib"
    "fmt"
)

var (
    fruits  = []string{"apple", "banana", "cherry", "durian"}
    banned = "durian"
)

func main() {
    channel := lib.PermutateWithChannel(fruits)
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            close(channel)
            //break
        }
    }
}

Примечание:

Оператор break (комментарий выше) не нужен, поскольку close(channel) вызывает range для возврата false в следующую итерацию, цикл завершается.

Проблема

Если вызывающему абоненту не нужны все перестановки, он должен явно указывать канал close(), или канал не будет закрыт до завершения программы (утечка ресурса). С другой стороны, если вызывающему абоненту нужны все перестановки (т.е. Цикл range до конца), вызывающий абонент НЕ ДОЛЖЕН close() канала. Это связано с тем, что close() -в уже закрытом канале вызывает панику (см. здесь в спецификации). Однако, если логика, чтобы определить, должна ли она остановиться или нет, не так проста, как показано выше, я думаю, что лучше использовать defer close(channel).

Вопросы

  • Каков идиоматический способ реализации таких генераторов?
  • Идиоматично, кто должен отвечать за close() канал - библиотечную функцию или вызывающего?
  • Это хорошая идея изменить мой код, как показано ниже, чтобы вызывающий абонент отвечал за defer close() канал независимо от того, что?

В библиотеке измените это:

    go func() {
        permutateWithChannel(channel, strings, prefix)
        close(channel)
    }()

:

    go permutateWithChannel(channel, strings, prefix)

В вызывающем абоненте измените это:

func main() {
    channel := lib.PermutateWithChannel(fruits)
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            close(channel)
        }
    }
}

:

func main() {
    channel := lib.PermutateWithChannel(fruits)
    defer close(channel)    // <- Added
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            break           // <- Changed
        }
    }
}
  1. Несмотря на то, что это не наблюдается, выполняя приведенный выше код, и правильность алгоритма не затрагивается, после вызова close() канала, goroutine, выполняющий код библиотеки, должен panic, когда он пытается отправить на замкнутый канал на следующей итерации, как описано здесь, в спецификации, в результате чего он заканчивается. Означает ли это какой-либо негативный побочный эффект?
  2. Подпись библиотечной функции func(strings []string) chan []string. В идеале тип возврата должен быть <-chan []string, чтобы ограничить его только получением. Однако если вызывающий абонент несет ответственность за close() за канал, он не может быть помечен как "только для приема", так как встроенная функция close() не работает в каналах только для приема. Каков идиоматический способ справиться с этим?
4b9b3361

Ответ 1

I. Альтернативы

Предисловие: Я буду использовать гораздо более простой генератор, потому что проблема не связана с сложностью генератора, а скорее с сигналами между генератором и потребителем и вызовом самого потребителя. Этот простой генератор генерирует только целые числа от 0 до 9.

1. С значением функции

Шаблон генерации-потребления намного чище с переданной простой потребительской функцией, которая также имеет то преимущество, что она может возвращать сигнализацию значения, если требуется аборт или любое другое действие.

И так как в примере должно быть указано только одно событие ( "прервать" ), потребительская функция будет иметь тип возврата bool, сигнализация, если требуется прерывание.

Итак, посмотрите этот простой пример с значением потребительской функции, переданным генератору:

func generate(process func(x int) bool) {
    for i := 0; i < 10; i++ {
        if process(i) {
            break
        }
    }
}

func main() {
    process := func(x int) bool {
        fmt.Println("Processing", x)
        return x == 3 // Terminate if x == 3
    }
    generate(process)
}

Вывод (попробуйте на Go Playground):

Processing 0
Processing 1
Processing 2
Processing 3

Обратите внимание, что потребитель (process) не должен быть "локальной" функцией, он может быть объявлен вне main(), например. это может быть глобальная функция или функция из другого пакета.

Потенциальным недостатком этого решения является то, что он использует только 1 goroutine как для генерирования, так и для потребления.

2. С каналами

Если вы все еще хотите сделать это с помощью каналов, вы можете. Обратите внимание, что поскольку канал создается генератором, и поскольку потребитель перебирает значения, полученные от канала (в идеале с конструкцией for ... range), ответственность за генератор за закрытие канала лежит на генераторе. Урегулирование с помощью этого также позволяет вам возвращать канал только для приема.

И да, закрытие возвращенного канала в генераторе лучше всего сделать как отложенный оператор, так что даже если генератор будет паниковать, потребитель не будет заблокирован. Но обратите внимание, что это отложенное закрытие не находится в функции generate(), а в анонимной функции, начинающейся с generate() и выполняемой как новый goroutine; иначе канал будет закрыт до того, как он будет возвращен из generate() - вообще не полезно...

И если вы хотите сигнализировать генератору от потребителя (например, чтобы прервать и не генерировать дополнительные значения), вы можете использовать, например, другой канал, который передается генератору. Поскольку генератор будет только "слушать" этот канал, он также может быть объявлен как канал только для приема генератору. Если вам нужно только сообщить одно событие (прервать в нашем случае), нет необходимости отправлять какие-либо значения на этом канале, это сделает простое закрытие. Если вам нужно сигнализировать о нескольких событиях, это можно сделать, фактически отправив значение на этот канал, событие/действие, которое должно выполняться (где прерывание может быть одним из нескольких событий).

И вы можете использовать оператор select как идиоматический способ обработки значений отправки на возвращенном канале и просмотра канала, переданного в генератор.

Вот решение с каналом abort:

func generate(abort <-chan struct{}) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        for i := 0; i < 10; i++ {
            select {
            case ch <- i:
                fmt.Println("Sent", i)
            case <-abort: // receive on closed channel can proceed immediately
                fmt.Println("Aborting")
                return
            }
        }
    }()
    return ch
}

func main() {
    abort := make(chan struct{})
    ch := generate(abort)
    for v := range ch {
        fmt.Println("Processing", v)
        if v == 3 { // Terminate if v == 3
            close(abort)
            break
        }
    }
    // Sleep to prevent termination so we see if other goroutine panics
    time.Sleep(time.Second)
}

Выход (попробуйте на Go Playground):

Sent 0
Processing 0
Processing 1
Sent 1
Sent 2
Processing 2
Processing 3
Sent 3
Aborting

Очевидным преимуществом этого решения является то, что он уже использует 2 goroutines (1, который генерирует значения 1, который потребляет/обрабатывает их), и его очень легко расширить, чтобы обрабатывать сгенерированные значения с любым количеством goroutines как канал, возвращаемый генератором, может использоваться одновременно из нескольких goroutines - каналы безопасны для приема одновременно, расы данных не могут возникать, по дизайну; для более читаемого: Если я правильно использую каналы, нужно ли мне использовать мьютексы?

II. Ответы на нерешенные вопросы

"невостребованная" паника на goroutine закончит выполнение goroutine, но не вызовет проблемы в отношении утечки ресурсов. Но если функция, выполняемая как отдельный goroutine, освободит ресурсы (в не отсроченных операторах), выделенные им в случае непаники, этот код, очевидно, не будет запущен и вызовет утечку ресурсов, например.

Вы этого не заметили, потому что программа завершается, когда главный горутин заканчивается (и он не дожидается завершения других не главных горутов, поэтому ваши другие горуты не получили паники). См. Spec: Выполнение программы.

Но знайте, что panic() и recover() предназначены для исключительных случаев, они не предназначены для таких общих случаев использования, как блоки Исключения и try-catch в Java. Паники следует избегать, например, возвращая ошибки (и обрабатывая их!), И паники, безусловно, не должны оставлять "границы" пакетов (например, panic() и recover() могут быть оправданы для использования в реализации пакета, но состояние паники должно быть "поймано" внутри пакета и не выпускаться из него).

Ответ 2

На мой взгляд, обычно генераторы - это просто обертки вокруг закрытия внутри. Что-то вроде этого

package main

import "fmt"

// This function `generator` returns another function, which
// we define anonymously in the body of `generator`. The
// returned function _closes over_ the variable `data` to
// form a closure.
func generator(data int, permutation func(int) int, bound int) func() (int, bool) {
    return func() (int, bool) {
        data = permutation(data)
        return data, data < bound
    }
}

// permutation function
func increment(j int) int {
    j += 1
    return j
}

func main() {
    // We call `generator`, assigning the result (a function)
    // to `next`. This function value captures its
    // own `data` value, which will be updated each time
    // we call `next`.
    next := generator(1, increment, 7)
    // See the effect of the closure by calling `next`
    // a few times.
    fmt.Println(next())
    fmt.Println(next())
    fmt.Println(next())
    // To confirm that the state is unique to that
    // particular function, create and test a new one.
    for next, generation, ok := generator(11, increment, 17), 0, true; ok; {
        generation, ok = next()
        fmt.Println(generation)
    }
}

Он выглядит не так элегантно, как "для диапазона", но достаточно ясен семантически и синтаксически для меня. И он работает http://play.golang.org/p/fz8xs0RYz9

Ответ 3

Я согласен с ответом icza. Подводя итог, есть две альтернативы:

  • функция сопоставления. Используйте обратный вызов для итерации по коллекции. func myIterationFn( yield func (myType)) (stopIterating bool). Это имеет недостаток, связанный с передачей потока управления функции myGenerator. myIterationFn не является генератором Pythonic, потому что он не возвращает итеративную последовательность.
  • каналы. Используйте канал и будьте осторожны с утечками goroutines. Можно преобразовать myIterationFn в функцию, которая возвращает итерируемую последовательность. Следующий пример служит примером такого преобразования.
myMapper := func(yield func(int) bool) {
    for i := 0; i < 5; i++ {
        if done := yield(i); done {
            return
        }
    }
}
iter, cancel := mapperToIterator(myMapper)
defer cancel() // This line is very important - it prevents goroutine leaks.
for value, ok := iter(); ok; value, ok = iter() {
    fmt.Printf("value: %d\n", value)
}

Здесь приведена полная программа. mapperToIterator преобразуется из функции отображения в генератор . Отсутствие дженериков требует отбрасывания от interface{} до int.

package main

import "fmt"

// yieldFn reports true if an iteration should continue. It is called on values
// of a collection.
type yieldFn func(interface{}) (stopIterating bool)

// mapperFn calls yieldFn for each member of a collection.
type mapperFn func(yieldFn)

// iteratorFn returns the next item in an iteration or the zero value. The
// second return value is true when iteration is complete.
type iteratorFn func() (value interface{}, done bool)

// cancelFn should be called to clean up the goroutine that would otherwise leak.
type cancelFn func()

// mapperToIterator returns an iteratorFn version of a mappingFn. The second
// return value must be called at the end of iteration, or the underlying
// goroutine will leak.
func mapperToIterator(m mapperFn) (iteratorFn, cancelFn) {
    generatedValues := make(chan interface{}, 1)
    stopCh := make(chan interface{}, 1)
    go func() {
        m(func(obj interface{}) bool {
            select {
            case <-stopCh:
                return false
            case generatedValues <- obj:
                return true
            }
        })
        close(generatedValues)
    }()
    iter := func() (value interface{}, notDone bool) {
        value, notDone = <-generatedValues
        return
    }
    return iter, func() {
        stopCh <- nil
    }
}

func main() {
    myMapper := func(yield yieldFn) {
        for i := 0; i < 5; i++ {
            if keepGoing := yield(i); !keepGoing {
                return
            }
        }
    }
    iter, cancel := mapperToIterator(myMapper)
    defer cancel()
    for value, notDone := iter(); notDone; value, notDone = iter() {
        fmt.Printf("value: %d\n", value.(int))
    }
}