J'ai une question sur l'écriture d'algorithmes récursifs dans un style fonctionnel. J'utiliserai Scala pour mon exemple ici, mais la question s'applique à tout langage fonctionnel.
Je suis en train de faire une énumération en profondeur d'une n -Arbre linéaire où chaque nœud possède une étiquette et un nombre variable d'enfants. Voici une implémentation simple qui imprime les étiquettes des nœuds feuilles.
case class Node[T](label:T, ns:Node[T]*)
def dfs[T](r:Node[T]):Seq[T] = {
if (r.ns.isEmpty) Seq(r.label) else for (n<-r.ns;c<-dfs(n)) yield c
}
val r = Node('a, Node('b, Node('d), Node('e, Node('f))), Node('c))
dfs(r) // returns Seq[Symbol] = ArrayBuffer('d, 'f, 'c)
Supposons maintenant que je veuille parfois pouvoir renoncer à l'analyse d'arbres trop grands en lançant une exception. Est-ce possible dans un langage fonctionnel ? Plus précisément, est-ce possible sans utiliser d'état mutable ? Cela semble dépendre de ce que vous entendez par "surdimensionné". Voici une version purement fonctionnelle de l'algorithme qui lève une exception lorsqu'il tente de traiter un arbre d'une profondeur de 3 ou plus.
def dfs[T](r:Node[T], d:Int = 0):Seq[T] = {
require(d < 3)
if (r.ns.isEmpty) Seq(r.label) else for (n<-r.ns;c<-dfs(n, d+1)) yield c
}
Mais que faire si un arbre est surdimensionné parce qu'il est trop large plutôt que trop profond ? Plus précisément, que se passe-t-il si je veux lancer une exception la n -pour la première fois, le dfs()
est appelée récursivement, quelle que soit la profondeur de la récursion ? La seule façon que je vois de faire cela est d'avoir un compteur mutable qui est incrémenté à chaque appel. Je ne vois pas comment le faire sans une variable mutable.
Je suis nouveau dans la programmation fonctionnelle et j'ai travaillé en supposant que tout ce que vous pouvez faire avec un état mutable peut être fait sans, mais je ne vois pas la réponse ici. La seule chose à laquelle je pense est d'écrire une version de dfs()
qui renvoie une vue sur tous les noeuds de l'arbre par ordre de profondeur.
dfs[T](r:Node[T]):TraversableView[T, Traversable[_]] = ...
Je pourrais alors imposer ma limite en disant dfs(r).take(n)
mais je ne vois pas comment écrire cette fonction. En Python, je créerais simplement un générateur par yield
à mesure que je visite les nœuds, mais je ne vois pas comment obtenir le même effet en Scala. (L'équivalent en Scala d'un style Python yield
semble être une fonction visiteur passée en paramètre, mais je n'arrive pas à trouver comment écrire une de ces fonctions qui génère une vue de séquence).
EDITAR On se rapproche de la réponse.
Voici une fonction qui renvoie un Stream
de nœuds dans l'ordre de la profondeur.
def dfs[T](r: Node[T]): Stream[Node[T]] = {
(r #:: Stream.empty /: r.ns)(_ ++ dfs(_))
}
C'est presque tout. Le seul problème est que Stream
mémorise tous les résultats, ce qui est un gaspillage de mémoire. Je veux une vue traversable. L'idée est la suivante, mais elle ne compile pas.
def dfs[T](r: Node[T]): TraversableView[Node[T], Traversable[Node[T]]] = {
(Traversable(r).view /: r.ns)(_ ++ dfs(_))
}
Il donne un message "trouvé TraversableView[Node[T], Traversable[Node[T]]]
nécessaire TraversableView[Node[T], Traversable[_]]
erreur pour le ++
opérateur. Si je change le type de retour en TraversableView[Node[T], Traversable[_]]
J'ai le même problème avec les clauses "found" et "required" interverties. Il y a donc une incantation magique de type variance que je n'ai pas encore trouvée, mais c'est presque ça.