web-dev-qa-db-fra.com

Le moyen idiomatique d'implémentation des rendements (rendement) dans Golang pour les fonctions récursives

[Note: J'ai lu Les générateurs de style Python dans Go , ce n'est pas une copie. ]

Dans Python/Ruby/JavaScript/ECMAScript 6, les fonctions du générateur pourraient être écrites à l'aide du mot clé yield fourni par le langage. Dans Go, il pourrait être simulé en utilisant un goroutine et un canal.

Le code

Le code suivant montre comment une fonction de permutation (abcd, abdc, acbd, acdb, ..., dcba) peut être implémentée:

// $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
}

Voici comment cela pourrait être utilisé:

// $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
        }
    }
}

Remarque:

L'instruction break (commentée ci-dessus) n'est pas nécessaire, car close(channel) force range à renvoyer false à la prochaine itération, la boucle se terminera.

Le problème

Si l'appelant n'a pas besoin de toutes les permutations, il doit expliciter close() le canal, sinon le canal ne sera pas fermé jusqu'à la fin du programme (une fuite de ressource se produit). D'autre part, si l'appelant a besoin de toutes les permutations (c'est-à-dire les boucles range jusqu'à la fin), l'appelant NE DOIT PAS close() le canal. C'est parce que close()- un canal déjà fermé provoque une panique à l'exécution (voir ici dans la spécification ). Cependant, si la logique permettant de déterminer s’il faut ou non s’arrêter n’est pas aussi simple que celle présentée ci-dessus, j’estime préférable d’utiliser defer close(channel).

Questions

  1. Quelle est la manière idiomatique d'implémenter des générateurs comme celui-ci?
  2. Idéalement, qui devrait être responsable de close() le canal - la fonction de bibliothèque ou l'appelant?
  3. Est-ce une bonne idée de modifier mon code comme ci-dessous, de sorte que l'appelant soit responsable de defer close() le canal, quoi qu'il arrive?

Dans la bibliothèque, modifiez ceci:

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

pour ça:

    go permutateWithChannel(channel, strings, prefix)

Dans l'appelant, modifiez ceci:

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

pour ça:

func main() {
    channel := lib.PermutateWithChannel(fruits)
    defer close(channel)    // <- Added
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            break           // <- Changed
        }
    }
}
  1. Bien que cela ne soit pas observable en exécutant le code ci-dessus et que l'exactitude de l'algorithme ne soit pas affectée, après l'appelant close() du canal, le goroutine exécutant le code de bibliothèque devrait panic lorsqu'il tente d'envoyer le canal fermé lors de la prochaine itération comme documenté ici dans la spécification , ce qui provoque sa fin. Est-ce que cela cause des effets secondaires négatifs?
  2. La signature de la fonction de bibliothèque est func(strings []string) chan []string. Idéalement, le type de retour devrait être <-chan []string pour le restreindre à la réception seulement. Toutefois, si c’est l’appelant qui est responsable de close() le canal, il ne peut pas être marqué comme "réception uniquement", car la fonction intégrée close() ne fonctionne pas sur les canaux de réception uniquement. Quelle est la manière idiomatique de gérer cela?

I. Alternatives

Avant-propos: Je vais utiliser un générateur beaucoup plus simple, car le problème ne concerne pas la complexité du générateur, mais plutôt les signaux entre le générateur et le consommateur et l'appel du consommateur lui-même. Ce générateur simple génère simplement les nombres entiers de 0 à 9.

1. avec une valeur de fonction

Un modèle générer-consommateur est beaucoup plus propre avec une simple fonction consommateur passée, ce qui présente également l'avantage de pouvoir renvoyer une valeur indiquant si un avortement ou toute autre action est nécessaire.

Et comme dans l'exemple, un seul événement doit être signalé ("abandon"), la fonction consommateur aura le type de retour bool, signalant si un abandon est requis.

Alors voyez cet exemple simple avec une valeur de fonction consommateur transmise au générateur:

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)
}

Sortie (essayez-le sur Go Playground ):

Processing 0
Processing 1
Processing 2
Processing 3

Notez que le consommateur (process) n'a pas besoin d'être une fonction "locale", il peut être déclaré en dehors de main(), par exemple. il peut s'agir d'une fonction globale ou d'une fonction d'un autre package.

L'inconvénient potentiel de cette solution est qu'elle n'utilise qu'un seul goroutine à la fois pour générer et consommer des valeurs. 

2. Avec des canaux

Si vous voulez toujours le faire avec des canaux, vous le pouvez. Notez que, puisque le canal est créé par le générateur et que le consommateur boucle sur les valeurs reçues du canal (idéalement avec une construction for ... range), il incombe au générateur de fermer le canal. Le fait de régler cela vous permet également de renvoyer un canal de réception seulement.

Et oui, il est préférable de fermer le canal renvoyé dans le générateur en tant qu'instruction différée. Ainsi, même en cas de panique du générateur, le consommateur ne sera pas bloqué. Mais notez que cette fermeture différée ne se trouve pas dans la fonction generate() mais dans la fonction anonyme démarrée à partir de generate() et exécutée comme un nouveau goroutine; sinon, le canal serait fermé avant d'être renvoyé par generate() - ce n'est pas du tout utile ...

Et si vous souhaitez signaler le générateur au consommateur (par exemple, pour abandonner et ne pas générer d'autres valeurs), vous pouvez utiliser par exemple. un autre canal, qui est transmis au générateur. Étant donné que le générateur "écoutera" uniquement ce canal, il peut également être déclaré comme canal de réception uniquement. Si vous avez seulement besoin de signaler un événement (abort dans notre cas), inutile d'envoyer des valeurs sur ce canal, une simple fermeture le fera. Si vous devez signaler plusieurs événements, vous pouvez le faire en envoyant une valeur sur ce canal, l'événement/l'action à exécuter (l'abandon pouvant en être un parmi plusieurs).

Et vous pouvez utiliser l'instruction select comme moyen idiomatique de gérer l'envoi de valeurs sur le canal renvoyé et de regarder le canal transmis au générateur.

Voici une solution avec un canal 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)
}

Sortie (essayez-le sur Go Playground ):

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

L’avantage évident de cette solution est qu’elle utilise déjà 2 goroutines (1 qui génère des valeurs, 1 qui les consomme/les traite) et qu’il est très facile de l’étendre pour traiter les valeurs générées avec un nombre quelconque de goroutines comme canal renvoyé par le générateur peut être utilisé simultanément par plusieurs goroutines - les canaux peuvent être reçus en même temps, les courses de données ne peuvent pas se produire, de par leur conception; for more read: Si j'utilise correctement les canaux, dois-je utiliser des mutex?

II. Réponses à des questions non traitées

Une panique «non appréhendée» sur un goroutine mettra fin à son exécution, mais ne posera pas de problème en ce qui concerne la fuite de ressources. Mais si la fonction exécutée en tant que goroutine distinct libérerait des ressources (dans des instructions non différées) allouées par celle-ci en cas de non panique, ce code ne s'exécutera évidemment pas et provoquera une fuite de ressources, par exemple.

Vous ne l'avez pas observé car le programme se termine à la fin du goroutine principal (et il n'attend pas que d'autres goroutines non principales soient terminées - vos autres goroutines n'ont donc pas eu le temps de paniquer). Voir Spec: Exécution du programme .

Mais sachez que panic() et recover() sont exceptionnellement, ils ne sont pas destinés à des cas d'utilisation aussi générale que les blocs Exceptions et try-catch en Java. Les paniques doivent être évités, en renvoyant les erreurs (et en les manipulant!) Par exemple, et les paniques ne doivent absolument pas quitter les "frontières" des paquets (par exemple, panic() et recover() peuvent être justifiés pour être utilisés dans une implémentation de paquet, "attrapé" à l'intérieur de l'emballage et ne pas laisser sortir).

19
icza

À mon avis, les générateurs ne sont généralement que des enveloppes autour de la fermeture interne. Quelque chose comme ça

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)
    }
}

Cela ne semble pas aussi élégant que «pour la portée» mais tout à fait clair pour moi sémantiquement et syntaxiquement. Et ça marche http://play.golang.org/p/fz8xs0RYz9

2
Uvelichitel

Je suis d'accord avec la réponse de icza. Pour résumer, il y a deux alternatives:

  1. fonction de cartographie: utilisez un rappel pour parcourir une collection. func myIterationFn(yieldfunc (myType)) (stopIterating bool). Cela a pour inconvénient de céder le flux de contrôle à la fonction myGenerator. myIterationFn n'est pas un générateur Pythonic car il ne renvoie pas de séquence itérable.
  2. channels: utilisez un canal et méfiez-vous des fuites de goroutines. Il est possible de transformer myIterationFn en une fonction qui retourne une séquence itérable. Le code suivant fournit un exemple d'une telle transformation.
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)
}

Voici un programme complet à titre d'exemple. mapperToIterator effectue la transformation d'une fonction mapping en un générateur. Le manque de génériques de Go nécessite un casting de 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