在Golang中为递归函数实现生成器(yield)的惯用方法

时间:2015-12-25 15:38:03

标签: recursion go generator yield

[注意:我读过Python-style generators in 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() - 已经关闭的通道导致运行时恐慌(请参阅here in the spec)。但是,如果确定是否应该停止的逻辑并不像上面所示那么简单,我认为最好使用defer close(channel)

问题

  1. 实现这样的生成器的惯用方法是什么?
  2. 以惯用语,谁应该对close()频道 - 图书馆功能或来电者负责?
  3. 如下所示修改我的代码是一个好主意,这样调用者就可以对defer close()频道负责吗?
  4. 在库中,修改:

        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当它尝试在下一次迭代中发送到关闭的通道,如文档here in the spec所示,导致它终止。这会导致任何负面副作用吗?
    2. 库函数的签名是func(strings []string) chan []string。理想情况下,返回类型应为<-chan []string,以将其限制为仅接收。但是,如果调用者对close()频道负责,则无法将其标记为&#34;仅接收&#34;,因为close()内置函数不起作用在仅接收频道上。解决这个问题的惯用方法是什么?

3 个答案:

答案 0 :(得分:20)

予。替代

前言:我将使用更简单的生成器,因为问题不涉及生成器的复杂性,而是发生器和消费者之间的信号,以及消费者本身的调用。这个简单的生成器只生成从09的整数。

1。带有功能值

通过简单的消费者功能,生成 - 消费者模式更加清晰,如果需要堕胎或任何其他行动,它还具有返回值信号的优势。

因为在示例中只有一个事件要发出信号(“abort”),所以消费者函数将具有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 statement作为惯用方法来处理在返回的频道上发送值并观看传递给发生器的频道。

以下是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个goroutine(1个生成值,1个消耗/处理它们),并且很容易扩展它以使用任意数量的goroutine处理生成的值作为生成器返回的通道可以同时从多个goroutine中使用 - 通道可以安全地同时接收,数据竞争不会发生,设计;更多阅读:If I am using channels properly should I need to use mutexes?

II。未解决问题的答案

goroutine上的“未捕获”恐慌将结束goroutine的执行,但不会导致资源泄漏问题。但是,如果作为单独的goroutine执行的函数在非恐慌的情况下释放由它分配的资源(在非延迟语句中),那么该代码显然不会运行并且例如会导致资源泄漏。

你没有观察到这一点,因为程序在主goroutine终止时终止(并且它不等待其他非主要goroutine完成 - 所以你的其他goroutines没有机会恐慌)。请参阅Spec: Program execution

但是要知道panic()recover()用于例外情况,它们不适用于Java中的异常和try-catch块等常规用例。应该避免恐慌,例如,通过返回错误(并处理它们!),恐慌绝对不应该离开包的“边界”(例如panic()recover()可能被证明可以用于包的实现,但恐慌状态应该被“抓住”在包内,而不是放弃它。)

答案 1 :(得分:3)

在我看来,通常生成器只是内部封闭的封装器。像这样的东西

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

答案 2 :(得分:1)

我同意icza的回答。总而言之,有两种选择:

  1. 映射功能:使用回调迭代集合。 func myIterationFn( yield func (myType)) (stopIterating bool)。这样做的缺点是将控制流程转为myGenerator功能。 myIterationFn不是Pythonic生成器,因为它不会返回可迭代序列。
  2. 频道:使用频道并警惕泄漏的goroutines。可以将myIterationFn转换为返回可迭代序列的函数。以下代码提供了此类转换的示例。
  3. 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))
        }
    }