web-dev-qa-db-fra.com

Quels sont les équivalents fonctionnels des instructions de rupture impératives et des autres vérifications de boucle?

Disons que j'ai la logique ci-dessous. Comment écrire cela dans la programmation fonctionnelle?

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                answer += e;
                if(answer == 10) break;
                if(answer == 150) answer += 100;
            }
        }
        return answer;
    }

Les exemples dans la plupart des blogs, articles ... Je vois ne fait qu'expliquer le cas simple d'une fonction mathématique simple: "Sum". Mais, j'ai une logique similaire à celle ci-dessus écrite en Java et je voudrais migrer cela vers du code fonctionnel dans Clojure. Si nous ne pouvons pas faire ce qui précède dans FP, alors le genre de promotions pour FP ne l'indique pas explicitement.

Je sais que le code ci-dessus est totalement impératif. Il n'a pas été écrit avec la prévoyance de le migrer vers FP à l'avenir.

36
Vicky

L'équivalent le plus proche d'une boucle sur un tableau dans la plupart des langages fonctionnels est une fonction fold, c'est-à-dire une fonction qui appelle une fonction spécifiée par l'utilisateur pour chaque valeur du tableau, en passant une valeur cumulée le long de la chaîne. Dans de nombreux langages fonctionnels, fold est complété par une variété de fonctions supplémentaires qui fournissent des fonctionnalités supplémentaires, y compris la possibilité de s'arrêter tôt lorsqu'une condition se présente. Dans les langages paresseux (par exemple Haskell), un arrêt précoce peut être obtenu simplement en n'évaluant pas davantage le long de la liste, ce qui ne générera jamais de valeurs supplémentaires. Par conséquent, en traduisant votre exemple en Haskell, je l'écrirais comme suit:

doSomeCalc :: [Int] -> Int
doSomeCalc values = foldr1 combine values
  where combine v1 v2 | v1 == 10  = v1
                      | v1 == 150 = v1 + 100 + v2
                      | otherwise = v1 + v2

Décomposer cette ligne par ligne au cas où vous n'êtes pas familier avec la syntaxe de Haskell, cela fonctionne comme ceci:

doSomeCalc :: [Int] -> Int

Définit le type de la fonction, en acceptant une liste d'entiers et en renvoyant un seul int.

doSomeCalc values = foldr1 combine values

Le corps principal de la fonction: argument donné values, retourne foldr1 Appelé avec les arguments combine (que nous définirons ci-dessous) et values. foldr1 Est une variante de la primitive de repli qui commence avec l'accumulateur défini sur la première valeur de la liste (d'où 1 Dans le nom de la fonction), puis la combine en utilisant la fonction spécifiée par l'utilisateur à partir de de gauche à droite (qui est généralement appelé un repli vers la droite , d'où le r dans le nom de la fonction). Donc foldr1 f [1,2,3] Est équivalent à f 1 (f 2 3) (ou f(1,f(2,3)) dans une syntaxe plus conventionnelle de type C).

  where combine v1 v2 | v1 == 10  = v1

Définition de la fonction locale combine: elle reçoit deux arguments, v1 Et v2. Lorsque v1 Vaut 10, il renvoie simplement v1. Dans ce cas, v2 n'est jamais évalué , donc la boucle s'arrête ici.

                      | v1 == 150 = v1 + 100 + v2

Alternativement, lorsque v1 est 150, ajoute 100 supplémentaires et ajoute v2.

                      | otherwise = v1 + v2

Et, si aucune de ces conditions n'est vraie, ajoute simplement v1 à v2.

Maintenant, cette solution est quelque peu spécifique à Haskell, car le fait qu'un pli droit se termine si la fonction de combinaison n'évalue pas son deuxième argument est provoqué par la stratégie d'évaluation paresseuse de Haskell. Je ne connais pas Clojure, mais je crois qu'il utilise une évaluation stricte, donc je m'attendrais à ce qu'il ait une fonction fold dans sa bibliothèque standard qui inclut un support spécifique pour la résiliation anticipée. Ceci est souvent appelé foldWhile, foldUntil ou similaire.

Un rapide coup d'œil à la documentation de la bibliothèque Clojure suggère qu'elle est un peu différente de la plupart des langages fonctionnels dans la dénomination, et que fold n'est pas ce que vous recherchez (c'est un mécanisme plus avancé visant à permettre le calcul parallèle ) mais reduce est l'équivalent le plus direct. Une résiliation anticipée se produit si la fonction reduced est appelée dans votre fonction de combinaison. Je ne suis pas sûr à 100% de comprendre la syntaxe, mais je soupçonne que ce que vous recherchez est quelque chose comme ceci:

(reduce 
    (fn [v1 v2]
        (if (= v1 10) 
             (reduced v1)
             (+ v1 v2 (if (= v1 150) 100 0))))
    array)

NB: les deux traductions, Haskell et Clojure, ne sont pas tout à fait correctes pour ce code spécifique; mais ils en donnent l'essentiel - voir la discussion dans les commentaires ci-dessous pour des problèmes spécifiques avec ces exemples.

45
Jules

Vous pouvez facilement le convertir en récursivité. Et il a un appel récursif optimisé pour la queue Nice.

Pseudocode:

public int doSomeCalc(int[] array)
{
    return doSomeCalcInner(array, 0);
}

public int doSomeCalcInner(int[] array, int answer)
{
    if (array is empty) return answer;

    // not sure how to efficiently implement head/tails array split in clojure
    var head = array[0] // first element of array
    var tail = array[1..] // remainder of array

    answer += head;
    if (answer == 10) return answer;
    if (answer == 150) answer += 100;

    return doSomeCalcInner(tail, answer);
}
33
Euphoric

J'aime vraiment réponse de Jules , mais je voulais également souligner quelque chose que les gens manquent souvent à propos de la programmation fonctionnelle paresseuse, à savoir que tout ne doit pas être "à l'intérieur de la boucle". Par exemple:

baseSums = scanl (+) 0

offsets = scanl (\offset sum -> if sum == 150 then offset + 100 else offset) 0

zipWithOffsets xs = zipWith (+) xs (offsets xs)

stopAt10 xs = if 10 `elem` xs then 10 else last xs

result = stopAt10 . zipWithOffsets . baseSums

result [1..]         -- 10
result [11..1000000] -- 500000499945

Vous pouvez voir que chaque partie de votre logique peut être calculée dans une fonction distincte puis composée ensemble. Cela permet des fonctions plus petites qui sont généralement beaucoup plus faciles à dépanner. Pour votre exemple de jouet, cela ajoute peut-être plus de complexité qu'il n'en supprime, mais dans le code du monde réel, les fonctions de séparation sont souvent beaucoup plus simples que l'ensemble.

13
Karl Bielefeldt

La plupart des exemples de traitement de liste que vous verrez utilisent des fonctions comme map, filter, sum etc. qui opèrent sur la liste dans son ensemble. Mais dans votre cas, vous avez une sortie anticipée conditionnelle - un modèle plutôt rare qui n'est pas pris en charge par les opérations de liste habituelles. Vous devez donc dérouler un niveau d'abstraction et utiliser la récursivité - ce qui est également plus proche de l'aspect de l'exemple impératif.

Ceci est une traduction plutôt directe (probablement pas idiomatique) dans Clojure:

(defn doSomeCalc 
  ([lst] (doSomeCalc lst 0))
  ([lst sum]
    (if (empty? lst) sum
        (if (= sum 10) sum
            (let [sum (+ sum (first lst))]
                 [sum (if (= sum 150) (+ sum 100) sum)]
               (recur (rest lst) sum))))))) 

Edit: Jules souligne que reduce dans Clojure do supporte la sortie anticipée. Son utilisation est plus élégante:

(defn doSomeCalc [lst]  
  (reduce (fn [sum val]
    (if (= sum 10) (reduced sum)
        (let [sum (+ sum val)]
             [sum (if (= sum 150) (+ sum 100) sum)]
           sum))
   lst)))

Dans tous les cas, vous pouvez faire n'importe quoi dans les langages fonctionnels comme vous le pouvez dans les langages impératifs, mais vous devez souvent changer quelque peu votre état d'esprit pour trouver une solution élégante. Dans le codage impératif, vous pensez à traiter une liste étape par étape, tandis que dans les langages fonctionnels, vous recherchez une opération à appliquer à tous les éléments de la liste.

6
JacquesB

Comme indiqué dans d'autres réponses, Clojure a reduced pour arrêter les réductions plus tôt:

(defn some-calc [coll]
  (reduce (fn [answer e]
            (let [answer (+ answer e)]
               (case answer
                 10  (reduced answer)
                 150 (+ answer 100)
                 answer)))
          0 coll))

C'est la meilleure solution pour votre situation particulière. Vous pouvez également obtenir beaucoup de kilométrage en combinant reduced avec transduce, ce qui vous permet d'utiliser des transducteurs de map, filter etc. Cependant, il est loin d'être une réponse complète à votre question générale.

Les suites d'échappement sont une version généralisée des instructions break et return. Ils sont directement implémentés dans certains schémas (call-with-escape-continuation), LISP commun (block + return, catch + throw) et même C (setjmp + longjmp). Des continuations plus générales délimitées ou non délimitées comme dans Scheme standard ou comme monades de continuation dans Haskell et Scala peut également être utilisée comme continuations d'échappement.

Par exemple, dans Racket, vous pouvez utiliser let/ec comme ça:

(define (some-calc ls)
  (let/ec break ; let break be an escape continuation
    (foldl (lambda (answer e)
             (let ([answer (+ answer e)])
               (case answer
                 [(10)  (break answer)] ; return answer immediately
                 [(150) (+ answer 100)]
                 [else  answer])))
           0 ls)))

De nombreux autres langages ont également échappé aux constructions de type continuation sous forme de gestion des exceptions. Dans Haskell, vous pouvez également utiliser l'une des différentes monades d'erreur avec foldM. Parce qu'il s'agit principalement de constructions de gestion d'erreurs utilisant des exceptions ou des monades d'erreur pour les premiers retours, elles sont généralement culturellement inacceptables et peut-être assez lentes.

Vous pouvez également passer des fonctions d'ordre supérieur aux appels de queue.

Lorsque vous utilisez des boucles, vous entrez automatiquement l'itération suivante lorsque vous atteignez la fin du corps de la boucle. Vous pouvez entrer l'itération suivante plus tôt avec continue ou quitter la boucle avec break (ou return). Lorsque vous utilisez des appels de queue (ou la construction loop de Clojure qui imite la récursivité de queue), vous devez toujours faire un appel explicite pour entrer l'itération suivante. Pour arrêter la boucle, vous ne faites pas l'appel récursif mais donnez la valeur directement:

(defn some-calc [coll]
  (loop [answer 0, [e es :as coll] coll]
    (if (empty? coll)
      answer
      (let [answer (+ answer e)]
        (case answer
          10 answer
          150 (recur (+ answer 100) es)
          (recur answer es))))))
4
nilern

La partie complexe est la boucle. Commençons par cela. Une boucle est généralement convertie en style fonctionnel en exprimant l'itération avec une seule fonction. Une itération est une transformation de la variable de boucle.

Voici une implémentation fonctionnelle d'une boucle générale:

loop : v -> (v -> v) -> (v -> Bool) -> v
loop init iter cond_to_cont = 
    if cond_to_cont init 
        then loop (iter init) iter cond
        else init

Il prend (une valeur initiale de la variable de boucle, la fonction qui exprime une seule itération [sur la variable de boucle]) (une condition pour continuer la boucle).

Votre exemple utilise une boucle sur un tableau, qui se rompt également. Cette capacité dans votre langue impérative est intégrée dans la langue elle-même. Dans la programmation fonctionnelle, cette capacité est généralement mise en œuvre au niveau de la bibliothèque. Voici une implémentation possible

module Array (foldlc) where

foldlc : v -> (v -> e -> v) -> (v -> Bool) -> Array e -> v
foldlc init iter cond_to_cont arr = 
    loop 
        (init, 0)
        (λ (val, next_pos) -> (iter val (at next_pos arr), next_pos + 1))
        (λ (val, next_pos) -> and (cond_to_cont val) (next_pos < size arr))

Dans ce document:

J'utilise une paire ((val, next_pos)) qui contient la variable de boucle visible à l'extérieur et la position dans le tableau, que cette fonction cache.

La fonction d'itération est légèrement plus complexe que dans la boucle générale, cette version permet d'utiliser l'élément courant du tableau. [C'est sous la forme curry .]

Ces fonctions sont généralement appelées "repli".

J'ai mis un "l" dans le nom pour indiquer que l'accumulation des éléments du tableau se fait de manière associative à gauche; pour imiter l'habitude des langages de programmation impératifs pour itérer un tableau d'index bas à élevé.

J'ai mis un "c" dans le nom pour indiquer que cette version de fold prend une condition qui contrôle si et quand la boucle doit être arrêtée tôt.

Bien entendu, ces fonctions utilitaires sont susceptibles d'être facilement disponibles dans la bibliothèque de base livrée avec le langage de programmation fonctionnel utilisé. Je les ai écrites ici pour démonstration.

Maintenant que nous avons tous les outils qui sont dans la langue dans le cas impératif, nous pouvons nous tourner pour implémenter la fonctionnalité spécifique de votre exemple.

La variable dans votre boucle est une paire ('réponse', un booléen qui encode s'il faut continuer).

iter : (Int, Bool) -> Int -> (Int, Bool)
iter (answer, cont) collection_element = 
  let new_answer = answer + collection_element
  in case new_answer of
    10 -> (new_answer, false)
    150 -> (new_answer + 100, true)
    _ -> (new_answer, true)

Notez que j'ai utilisé une nouvelle "variable" 'new_answer'. En effet, dans la programmation fonctionnelle, je ne peux pas changer la valeur d'une "variable" déjà initialisée. Je ne m'inquiète pas des performances, le compilateur peut réutiliser la mémoire de 'réponse' pour 'new_answer' via l'analyse de la durée de vie, s'il pense que c'est plus efficace.

L'intégration de cela dans notre fonction de boucle développée précédemment:

doSomeCalc :: Array Int -> Int
doSomeCalc arr = fst (Array.foldlc (0, true) iter snd arr)

"Array" est ici le nom du module qui exporte la fonction foldlc.

"fist", "second" représente les fonctions qui renvoient le premier, deuxième composant de son paramètre paire

fst : (x, y) -> x
snd : (x, y) -> y

Dans ce cas, le style "sans point" augmente la lisibilité de l'implémentation de doSomeCalc:

doSomeCalc = Array.foldlc (0, true) iter snd >>> fst

(>>>) est la composition de la fonction: (>>>) : (a -> b) -> (b -> c) -> (a -> c)

C'est la même chose que ci-dessus, seul le paramètre "arr" est omis des deux côtés de l'équation de définition.

Une dernière chose: vérifier la casse (tableau == null). Dans des langages de programmation mieux conçus, mais même dans des langages mal conçus avec une certaine discipline de base, on utilise plutôt un type facultatif pour exprimer la non-existence. Cela n'a pas grand-chose à voir avec la programmation fonctionnelle, dont la question est finalement, donc je ne m'en occupe pas.

2
libeako

Tout d'abord, réécrivez légèrement la boucle, de sorte que chaque itération de la boucle soit une sortie anticipée, soit mute answer exactement une fois:

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                if(answer + e == 10) return answer + e;
                else if(answer + e == 150) answer = answer + e + 100;
                else answer = answer + e;
            }
        }
        return answer;
    }

Il devrait être clair que le comportement de cette version est exactement le même qu'avant, mais maintenant, il est beaucoup plus simple de convertir en style récursif. Voici une traduction directe de Haskell:

doSomeCalc :: [Int] -> Int
doSomeCalc = recurse 0
  where recurse :: Int -> [Int] -> Int
        recurse answer [] = answer
        recurse answer (e:array)
          | answer + e == 10 = answer + e
          | answer + e == 150 = recurse (answer + e + 100) array
          | otherwise = recurse (answer + e) array

Maintenant, c'est purement fonctionnel, mais nous pouvons l'améliorer du point de vue de l'efficacité et de la lisibilité en utilisant un pli au lieu d'une récursivité explicite:

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer + e == 10 = Left (answer + e)
          | answer + e == 150 = Right (answer + e + 100)
          | otherwise = Right (answer + e)

Dans ce contexte, Left sort tôt avec sa valeur et Right continue la récursivité avec sa valeur.


Cela pourrait maintenant être simplifié un peu plus, comme ceci:

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer' == 10 = Left 10
          | answer' == 150 = Right 250
          | otherwise = Right answer'
          where answer' = answer + e

C'est mieux que le code Haskell final, mais il est maintenant un peu moins clair de voir comment il correspond au Java d'origine.