Disons, 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, des articles ... Je vois juste expliquer le cas simple d'une fonction mathématique simple dit «Somme». Mais, j’ai une logique similaire à celle décrite ci-dessus en Java et j’aimerais la migrer vers du code fonctionnel dans Clojure. Si nous ne pouvons pas faire ce qui précède dans FP, le type 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 dans le futur.
break
etreturn answer
peut être remplacée par unereturn
boucle interne. Dans FP, vous pouvez implémenter ce retour rapide en utilisant des continuations, voir par exemple en.wikipedia.org/wiki/ContinuationtakeWhile
.Réponses:
L'équivalent le plus proche du bouclage sur un tableau dans la plupart des langages fonctionnels est une
fold
fonction, c'est-à-dire une fonction qui appelle une fonction spécifiée par l'utilisateur pour chaque valeur du tableau, en transmettant une valeur accumulée le long de la chaîne. Dans de nombreux langages fonctionnels,fold
diverses fonctions supplémentaires, telles que la possibilité d’arrêter plus tôt lorsque survient une condition, viennent s’ajouter à celle-ci. Dans les langages paresseux (par exemple, Haskell), il est possible d’arrêter tôt en évitant d’évaluer la suite de la liste, ce qui évitera de générer des valeurs supplémentaires. Par conséquent, en traduisant votre exemple en Haskell, je l’écrirais ainsi:Découper ceci ligne par ligne au cas où vous ne seriez pas familier avec la syntaxe de Haskell, ceci fonctionne comme suit:
Définit le type de la fonction, accepte une liste d'ints et renvoie un seul int.
Le corps principal de la fonction: argument donné
values
, returnfoldr1
appelé avec argumentscombine
(que nous définirons plus bas) etvalues
.foldr1
est une variante de la primitive fold qui commence par la pile définie sur la première valeur de la liste (d’où le nom1
de la fonction), puis la combine en utilisant la fonction spécifiée par l'utilisateur de gauche à droite (généralement appelée fold à droite , d'où ler
dans le nom de la fonction). Donc,foldr1 f [1,2,3]
est équivalent àf 1 (f 2 3)
(ouf(1,f(2,3))
dans la syntaxe plus conventionnelle C-like).Définir la
combine
fonction locale: il reçoit deux arguments,v1
etv2
. Quandv1
est 10, il revient justev1
. Dans ce cas, la v2 n'est jamais évaluée , la boucle s'arrête ici.Sinon, lorsque v1 est égal à 150, ajoute 100 points supplémentaires et ajoute 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 repli droit se termine si la fonction de combinaison n'évalue pas son second argument est causé par la stratégie d'évaluation paresseuse de Haskell. Je ne connais pas Clojure, mais je crois qu’elle utilise une évaluation stricte. Je pense donc qu’elle devrait disposer d’une
fold
fonction 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 en nommant, et que ce
fold
n’est pas ce que vous recherchez (c’est un mécanisme plus avancé visant à permettre le calcul parallèle), maisreduce
plus directe. équivalent. La fin anticipée se produit si lareduced
fonction 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 ressemble à ceci: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 liés à ces exemples.
la source
v1 v2
sont déroutants:v1
est une "valeur de tableau", maisv2
est le résultat accumulé. et votre traduction est erronée, je crois, les sorties de boucle OP lorsque le cumul (à gauche) la valeur montres 10, pas un élément du tableau. Idem avec l’augmentation de 100. Si vous utilisez les plis ici, utilisez le pli gauche avec sortie anticipée, avec quelques variations par rapport àfoldlWhile
ici .(= v1 150)
utilise la valeur avantv2
(ou.e
) Lui est ajoutée .Breaking this down line by line in case you're not familiar with Haskell's syntax
-- Tu es mon héros. Haskell est un mystère pour moi.Vous pouvez facilement le convertir en récursivité. Et il a beau appel récursif queue-optimisé.
Pseudocode:
la source
GOTO
. (Pas si mal, mais quand même assez gênant.) L'équivalent d'une boucle, comme dit Jules, est un pli approprié.GOTO
. Dans tous les cas, lorsque vous compilez la récursion de la queue, cela revient généralement à unewhile (true)
boucle avec le corps de la fonction dans lequel le retour anticipé n'est qu'unebreak
déclaration. Un pli, bien que vous ayez raison de dire qu'il s'agit d'une boucle, est en réalité plus contraint qu'une construction en boucle générale; cela ressemble plus à une boucle pour-chaqueGOTO
que vous devez faire une comptabilité fastidieuse de quels arguments dans quel état sont passés à l'appel récursif, pour s'assurer qu'il se comporte réellement comme prévu. Cela n’est pas nécessaire dans la même mesure dans les boucles impératives écrites avec décence (où sont clairement définies les variables avec état et comment elles changent à chaque itération), ni dans la récursion naïve (où l’on ne fait généralement pas grand chose avec les arguments). le résultat est assemblé de manière assez intuitive). ...J'aime beaucoup la réponse de Jules , mais je voulais aussi souligner quelque chose qui manque souvent à la programmation fonctionnelle paresseuse, à savoir que tout ne doit pas forcément être "dans la boucle". Par exemple:
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 à résoudre. 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.
la source
stopAt10
n'est pas un bon consommateur. votre réponse est meilleure que celle que vous citez, car elle isole correctement le producteurscanl (+) 0
de base de valeurs. leur consommation devrait incorporer directement la logique de contrôle, cependant, il vaut mieux la mettre en œuvre avec seulement deuxspan
secondes et unlast
, explicitement. cela suivrait de près la structure et la logique du code original et serait facile à maintenir.La plupart des exemples de traitement de la liste , vous verrez des fonctions d'utilisation comme
map
,filter
,sum
etc. , qui fonctionnent sur la liste dans son ensemble. Mais dans votre cas, vous avez une sortie anticipée conditionnelle - un modèle plutôt inhabituel qui n'est pas pris en charge par les opérations de liste habituelles. Vous devez donc définir un niveau d'abstraction et utiliser la récursivité, ce qui est également plus proche de la présentation de l'exemple impératif.Ceci est une traduction plutôt directe (probablement pas idiomatique) de Clojure:
Edit: Jules fait remarquer qu’à
reduce
Clojure , les sorties anticipées sont possibles. Utiliser ceci est plus élégant:Dans tous les cas, vous pouvez faire n'importe quoi dans des langages fonctionnels comme dans des langages impératifs, mais vous devez souvent changer quelque peu votre mental 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.
la source
reduce
opération de Clojure prend en charge les sorties anticipées.takeWhile
pas une «opération commune»?takeWhile
soit une opération courante, ce n'est pas particulièrement utile dans ce cas, car vous avez besoin des résultats de votre transformation avant de pouvoir décider d'arrêter ou non. Dans un langage paresseux, cela n'a pas d'importance: vous pouvez utiliserscan
ettakeWhile
sur les résultats de l'analyse (voir la réponse de Karl Bielefeldt, qui, bien qu'il ne l'utilise pas,takeWhile
pourrait facilement être réécrite pour le faire), mais pour un langage strict comme clojure, signifie traiter toute la liste et ensuite éliminer les résultats. Les fonctions de générateur pourraient toutefois résoudre ce problème, et je pense que clojure les prend en charge.take-while
in Clojure produit une séquence lente (selon la documentation). Une autre façon de résoudre ce problème serait d'utiliser des transducteurs (peut-être le meilleur).Comme le soulignent d’autres réponses, Clojure doit,
reduced
pour arrêter les réductions à un stade précoce:C'est la meilleure solution pour votre situation particulière. Vous pouvez également obtenir beaucoup de temps en combinant
reduced
avectransduce
, ce qui vous permet d'utiliser des transducteursmap
,filter
etc. Cependant, la réponse à votre question générale est loin d'être complète.Les suites d'échappement sont une version généralisée des instructions break et return. Ils sont directement implémentés dans certains Schemes (
call-with-escape-continuation
), Common Lisp (block
+return
,catch
+throw
) et même C (setjmp
+longjmp
). Des suites plus générales délimitées ou non délimitées, telles que définies dans le schéma standard ou comme monades de continuation dans Haskell et Scala, peuvent également être utilisées en tant que suites d'échappement.Par exemple, dans Racket, vous pourriez utiliser
let/ec
comme ceci:De nombreux autres langages ont également des constructions de type continuation, sous forme de gestion des exceptions. En Haskell, vous pouvez également utiliser l'une des différentes monades d'erreur
foldM
. Parce qu’il s’agit principalement de structures de traitement des erreurs utilisant des exceptions ou des monades d’erreur pour les retours anticipés, elles sont généralement inacceptables du point de vue culturel et peuvent être très lentes.Vous pouvez également passer des fonctions d’ordre supérieur aux appels en attente.
Lorsque vous utilisez des boucles, vous entrez automatiquement la prochaine itération lorsque vous atteignez la fin du corps de la boucle. Vous pouvez entrer tôt la prochaine itération avec
continue
ou quitter la boucle avecbreak
(oureturn
). Lorsque vous utilisez des appels de queue (ou laloop
construction de Clojure qui imite la récursion de queue), vous devez toujours effectuer un appel explicite pour entrer l'itération suivante. Pour arrêter la lecture en boucle, ne passez pas l'appel récursif, mais donnez directement la valeur:la source
MonadError
, l'équivalent en substanceEither
n'a pas un tel parti pris pour la gestion des erreurs uniquement, et peut donc facilement être utilisé à la place.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:
Cela 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 casse également. Cette capacité dans votre langue impérative est incorporée dans la langue elle-même. En programmation fonctionnelle, une telle capacité est généralement implémentée au niveau de la bibliothèque. Voici une implémentation possible
En cela:
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. [Il est sous forme de curry .]
De telles fonctions sont généralement appelées "fold".
Je mets un "l" dans le nom pour indiquer que l'accumulation des éléments du tableau se fait de manière associative à gauche; imiter l'habitude des langages de programmation impératifs pour itérer un tableau d'index faible à élevé.
Je mets 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 plus 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 leur ai écrit ici pour démonstration.
Maintenant que nous avons tous les outils qui sont dans la langue dans le cas impératif, nous pouvons à présent mettre en œuvre les fonctionnalités spécifiques de votre exemple.
La variable dans votre boucle est une paire ('answer', un booléen qui indique s'il faut continuer).
Notez que j'ai utilisé une nouvelle "variable" 'new_answer'. En effet, en 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 pourrait réutiliser la mémoire de 'answer' pour 'new_answer' via une analyse à vie, s'il pense que cela est plus efficace.
En incorporant cela dans notre fonction de boucle développée précédemment:
"Array" est le nom du module qui exporte la fonction foldlc.
"poing", "deuxième" représente les fonctions qui retournent la première, deuxième composante de son paramètre de paire
Dans ce cas, le style "sans point" augmente la lisibilité de l'implémentation de doSomeCalc:
(>>>) est la composition de la fonction:
(>>>) : (a -> b) -> (b -> c) -> (a -> c)
C'est la même chose que ci-dessus, juste le paramètre "arr" est omis des deux côtés de l'équation qui définit.
Une dernière chose: vérifier la casse (array == null). Dans des langages de programmation mieux conçus, mais même dans des langages mal conçus avec une discipline de base, on utilise plutôt un type optionnel pour exprimer la non-existence. Cela n’a pas grand-chose à voir avec la programmation fonctionnelle, qui est au coeur de la question, donc je ne la traite pas.
la source
Tout d’abord, réécrivez légèrement la boucle, de sorte que chaque itération de la boucle se termine tôt ou mute
answer
exactement une fois:Il devrait être clair que le comportement de cette version est exactement le même qu'auparavant, mais maintenant, il est beaucoup plus simple de convertir en style récursif. Voici une traduction directe en Haskell:
Désormais, il est purement fonctionnel, mais nous pouvons l’améliorer à la fois en termes d’efficacité et de lisibilité en utilisant un repli au lieu d’une récursion explicite:
Dans ce contexte, les
Left
sorties prématurées avec sa valeur etRight
la récursion continue avec sa valeur.Cela pourrait maintenant être simplifié un peu plus, comme ceci:
C'est mieux en tant que code Haskell final, mais la façon dont il est renvoyé au Java d'origine est maintenant un peu moins claire.
la source