web-dev-qa-db-ja.com

再帰関数のGolangでジェネレーター(yield)を実装する慣用的な方法

[注:私は GoのPythonスタイルのジェネレーター を読みましたが、これはそれの複製ではありません。 ]

Python/Ruby/JavaScript/ECMAScript 6では、ジェネレーター関数は、言語が提供するyieldキーワードを使用して記述できます。Goでは、ゴルーチンとチャネルを使用してシミュレートできます。

コード

次のコードは、順列関数(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()してはなりません(MUST NOT)。これは、すでに閉じられているチャネルをclose()- ingするとランタイムパニックが発生するためです( 仕様のここで を参照)。ただし、停止する必要があるかどうかを判断するロジックが上記のように単純でない場合は、defer close(channel)を使用することをお勧めします。

質問

  1. このようなジェネレータを実装する慣用的な方法は何ですか?
  2. 慣用的に、誰がチャネルのclose()を担当する必要がありますか-ライブラリ関数または呼び出し元?
  3. 以下のようにコードを変更して、呼び出し元がチャネルの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() sした後、ライブラリコードを実行するゴルーチンはpanicを実行する必要があります文書化されているように、次の反復でクローズドチャネルに送信します ここでは仕様 。これにより悪影響が生じますか?
  2. ライブラリ関数のシグネチャはfunc(strings []string) chan []stringです。理想的には、戻り値の型を_<-chan []string_にして、受信専用に制限する必要があります。ただし、チャネルのclose()を担当するのが呼び出し側である場合、close()組み込み関数が機能しないため、「受信専用」としてマークできませんでした。受信専用チャネル。これに対処する慣用的な方法は何ですか?

I.代替案

まえがき:問題はジェネレーターの複雑さではなく、ジェネレーターとコンシューマー間の信号、および呼び出しに関係しないため、私ははるかに単純なジェネレーターを使用します消費者自身の。この単純なジェネレータは、_0_から_9_までの整数を生成するだけです。

1.関数値を使用

単純なconsumer関数を渡すと、generate-consumerパターンがよりクリーンになり、中絶やその他のアクションが必要な場合にシグナル値を返すことができるという利点もあります。

また、例では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()から返される前に閉じられます-まったく役に立ちません...

そして、コンシューマーからジェネレーターに信号を送りたい場合(例えば、中止してそれ以上の値を生成しない場合)は、例えばジェネレータに渡される別のチャネル。ジェネレータはこのチャネルのみを「リッスン」するため、ジェネレータへの受信専用チャネルとして宣言することもできます。 1つのイベント(この場合は打ち切り)を通知するだけでよく、このチャネルで値を送信する必要がない場合は、単純なクローズでそれを行います。複数のイベントを通知する必要がある場合、実際にこのチャネルに値を送信することで実行できます。実行するイベント/アクションです(中止は複数のイベントからの1つです)。

そして 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つのゴルーチン(1つは値を生成し、1つはそれらを消費/処理する)をすでに使用していることであり、それを拡張して、任意の数のゴルーチンを使用して生成された値を処理することは、ジェネレータは複数のゴルーチンから同時に使用できます-チャネルは同時に受信しても安全です。設計上、データの競合は発生しません。続きを読む: チャネルを適切に使用している場合、ミューテックスを使用する必要がありますか?

II。未解決の質問への回答

Goroutineで「キャッチされていない」パニックが発生すると、goroutineの実行は終了しますが、リソースリークに関して問題は発生しません。しかし、別のgoroutineとして実行された関数が、パニックでない場合に割り当てられたリソース(非据え置きステートメント内)を解放すると、そのコードは明らかに実行されず、たとえばリソースリークが発生します。

メインのゴルーチンが終了するとプログラムが終了するので、これを観察していません(他の非メインゴルーチンが終了するのを待たないため、他のゴルーチンはパニックする機会がありませんでした)。 仕様:プログラムの実行 を参照してください。

ただし、panic()およびrecover()は例外的なケースを対象としているため、JavaのExceptionsブロックや_try-catch_ブロックなどの一般的なユースケースは対象としていません。たとえば、エラーを返す(そしてエラーを処理する)ことでパニックを回避する必要があります。パニックは、パッケージの「境界」を残さないようにする必要があります(たとえば、panic()およびrecover()は、パッケージの実装で使用されますが、パニック状態はパッケージ内で「キャッチ」され、パッケージから解放されません)。

22
icza

私の考えでは、通常、ジェネレーターは内部的にクロージャーのラッパーにすぎません。このようなもの

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

5
Uvelichitel

Iczaの答えに同意します。要約すると、2つの選択肢があります。

  1. マッピング関数:コールバックを使用してコレクションを反復します。 _func myIterationFn(_yieldfunc (myType)) (stopIterating bool)。これには、制御フローをmyGenerator関数に譲ることの欠点があります。 myIterationFnは、反復可能なシーケンスを返さないため、Pythonicジェネレーターではありません。
  2. channels:チャネルを使用し、ゴルーチンのリークに注意してください。 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マッピング関数からジェネレータへの変換を行います。 Goにジェネリックがないため、_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))
    }
}
_
1
gonzojive