Je lisais un article sur la programmation fonctionnelle où l'écrivain déclare
(take 25 (squares-of (integers)))
Notez qu'il n'a pas de variables. En effet, il n'a que trois fonctions et une constante. Essayez d'écrire les carrés d'entiers en Java sans utiliser de variable. Oh, il y a probablement un moyen de le faire, mais ce n'est certainement pas naturel, et il ne serait pas aussi bien lu que mon programme ci-dessus.
Est-il possible d'y parvenir en Java? Supposons que vous deviez imprimer les carrés des 15 premiers entiers, pourriez-vous écrire une boucle for ou while sans utiliser de variables?
Avis de mod
Cette question n'est pas un concours de golf à code. Nous recherchons des réponses qui expliquent les concepts impliqués (idéalement sans répéter les réponses précédentes), et pas seulement pour encore un autre morceau de code.
la source
squaresOf(integers()).take(25)
(l'écriture de ces fonctions reste un exercice pour le lecteur; la difficulté réside dans l'ensemble infini pourintegers()
, mais c'est un problème pour Java en raison de son évaluation avide, rien à voir avec variables)Réponses:
Est-il possible d'implémenter un tel exemple en Java sans utiliser de mises à jour destructrices? Oui. Cependant, comme @Thiton et l'article lui-même l'ont mentionné, ce sera moche (selon les goûts). Une façon utilise la récursivité; voici un exemple Haskell qui fait quelque chose de similaire:
Remarque 1) le manque de mutation, 2) l'utilisation de la récursivité et 3) le manque de boucles. Le dernier point est très important - les langages fonctionnels n'ont pas besoin de constructions en boucle intégrées au langage, car la récursivité peut être utilisée pour la plupart (tous?) Des cas où des boucles sont utilisées en Java. Voici une série bien connue d'articles montrant à quel point les appels de fonction sont incroyablement expressifs.
J'ai trouvé l'article insatisfaisant et j'aimerais faire quelques remarques supplémentaires:
Cet article est une explication très pauvre et déroutante de la programmation fonctionnelle et de ses avantages. Je recommanderais fortement d' autres sources pour en savoir plus sur la programmation fonctionnelle.
La partie la plus déroutante de l'article est qu'elle ne mentionne pas qu'il existe deux utilisations pour les instructions d'affectation en Java (et dans la plupart des autres langages traditionnels):
lier une valeur à un nom:
final int MAX_SIZE = 100;
mise à jour destructrice:
int a = 3; a += 1; a++;
La programmation fonctionnelle évite la seconde, mais embrasse la première (exemples:
let
-expressions, paramètres de fonction,define
itions de niveau supérieur ) . Ceci est un point très important à saisir, car sinon l'article semble tout simplement ridicule et peut vous laisser se demander, quels sonttake
,squares-of
etintegers
si pas les variables?De plus, l'exemple n'a pas de sens. Il ne montre pas les mises en œuvre de
take
,squares-of
ouintegers
. Pour tout ce que nous savons, ils sont implémentés à l'aide de variables mutables. Comme l'a dit @Martin, vous pouvez trivialement écrire cet exemple en Java.Encore une fois, je recommanderais d'éviter cet article et d'autres comme celui-ci si vous voulez vraiment en savoir plus sur la programmation fonctionnelle. Il semble être écrit davantage dans le but de choquer et d'offenser plutôt que d'enseigner des concepts et des fondamentaux. Au lieu de cela, pourquoi ne pas consulter l' un de mes papiers préférés de tous les temps , par John Hughes. Hughes essaie de s'attaquer à certaines des mêmes questions que l'article couvert (bien que Hughes ne parle pas de concurrence / parallélisation); voici un teaser:
la source
take 25 (map (^2) [1..])
comme exemple?[1..]
? C'est une fonctionnalité intéressante intégrée à Haskell, mais n'illustre pas les concepts derrière la génération d'une telle liste. Je suis sûr que les instances de laEnum
classe (dont cette syntaxe nécessite) sont également utiles, mais étaient trop paresseuses pour les trouver. Ainsi,unfoldr
. :)Tu ne le ferais pas. Les variables sont au cœur de la programmation impérative, et si vous essayez de programmer impérativement sans utiliser de variables, vous causez juste une douleur au cul à tout le monde. Dans différents paradigmes de programmation, les styles sont différents et différents concepts forment votre base. Une variable en Java, lorsqu'elle est bien utilisée avec une petite portée, n'est pas un mal. Demander un programme Java sans variables, c'est comme demander un programme Haskell sans fonctions, donc vous ne le demandez pas, et vous ne vous laissez pas berner en voyant la programmation impérative comme inférieure car elle utilise des variables.
Ainsi, la manière Java serait:
et ne vous laissez pas berner pour l'écrire de manière plus complexe en raison d'une haine des variables.
la source
Le plus simple que je puisse faire avec la récursivité est une fonction avec un paramètre. Ce n'est pas très Java, mais cela fonctionne:
la source
Dans votre exemple fonctionnel, nous ne voyons pas comment les fonctions
squares-of
ettake
sont implémentées. Je ne suis pas un expert Java, mais je suis sûr que nous pourrions écrire ces fonctions pour activer une déclaration comme celle-ci ...ce qui n'est pas si différent.
la source
squares-of
n'est pas un nom valide en Java (l'squares_of
est cependant). Mais sinon, bon point qui montre que l'exemple de l'article est mauvais.integer
génère paresseusement des entiers, et latake
fonction choisit 25 dessquared-of
nombresinteger
. En bref, vous devriez avoir uneinteger
fonction pour produire paresseusement des entiers à l'infini.(integer)
une fonction - une fonction est toujours quelque chose qui mappe un argument à une valeur. Il s'avère que ce(integer)
n'est pas une fonction, mais simplement une valeur. On pourrait même aller jusqu'à dire queinteger
c'est une variable qui est liée à un nombre infini de nombres.En Java, vous pouvez le faire (en particulier la partie liste infinie) avec des itérateurs. Dans l'exemple de code suivant, le nombre fourni au
Take
constructeur peut être arbitrairement élevé.Ou avec des méthodes d'usine chaînables:
Où
SquaresOf
,Take
etIntegers
étendreNumbers
la source
while (test.hasNext()) System.out.println(test.next())
seraient un non-non dans FP; les itérateurs sont intrinsèquement mutables.Version courte:
Pour que le style à affectation unique fonctionne de manière fiable en Java, vous aurez besoin (1) d'une sorte d'infrastructure conviviale et (2) d'une prise en charge au niveau du compilateur ou de l'exécution pour l'élimination des appels de queue.
Nous pouvons écrire une grande partie de l'infrastructure et nous pouvons arranger les choses pour éviter de remplir la pile. Mais tant que chaque appel prend une trame de pile, il y aura une limite sur la quantité de récursivité que vous pouvez faire. Gardez vos itérables petits et / ou paresseux, et vous ne devriez pas avoir de problèmes majeurs. Au moins la plupart des problèmes que vous rencontrerez ne nécessitent pas de renvoyer un million de résultats à la fois. :)
Notez également que, comme le programme doit réellement effectuer des modifications visibles pour être exécuté, vous ne pouvez pas tout rendre immuable. Vous pouvez, cependant, garder la grande majorité de vos propres trucs immuables, en utilisant un minuscule sous-ensemble de mutables essentiels (flux, par exemple) uniquement à certains points clés où les alternatives seraient trop onéreuses.
Version longue:
Autrement dit, un programme Java ne peut pas totalement éviter les variables s'il veut faire quelque chose qui en vaille la peine. Vous pouvez les contenir et limiter ainsi la mutabilité dans une large mesure, mais la conception même du langage et de l'API - ainsi que la nécessité de changer éventuellement le système sous-jacent - rendent l'immuabilité totale irréalisable.
Java a été conçu dès le départ comme un impératif , orienté objet langage.
while (true)
etfor (;;)
! - dépendent totalement d'une variable qui change quelque part d'itération en itération.Le résultat final de ces décisions de conception est que sans variables mutables, Java n'a aucun moyen de changer l'état de quoi que ce soit - même quelque chose d'aussi simple que d'imprimer "Hello world!" à l'écran implique un flux de sortie, ce qui implique de coller des octets dans un tampon mutable .
Donc, à toutes fins pratiques, nous sommes limités à bannir les variables de notre propre code. OK, on peut faire ça. Presque. Fondamentalement, nous aurions besoin de remplacer presque toutes les itérations par la récursivité et toutes les mutations par des appels récursifs renvoyant la valeur modifiée. ainsi...
Fondamentalement, nous construisons une liste liée, où chaque nœud est une liste en soi. Chaque liste a une "tête" (la valeur actuelle) et une "queue" (la sous-liste restante). La plupart des langages fonctionnels font quelque chose de similaire, car ils se prêtent très bien à une immuabilité efficace. Une opération "suivante" renvoie simplement la queue, qui est généralement passée au niveau suivant dans une pile d'appels récursifs.
Maintenant, c'est une version extrêmement simpliste de ce genre de choses. Mais c'est assez bon pour démontrer un problème sérieux avec cette approche en Java. Considérez ce code:
Bien que nous n'ayons besoin que de 25 pouces pour le résultat,
squares_of
je ne le sais pas. Il va retourner le carré de chaque nombreintegers
. La récursivité de 20 millions de niveaux de profondeur cause de gros problèmes en Java.Vous voyez, les langages fonctionnels dans lesquels vous feriez généralement de la folie comme ça, ont une fonctionnalité appelée "élimination des appels de queue". Cela signifie que lorsque le compilateur voit que le dernier acte du code consiste à s'appeler lui-même (et à renvoyer le résultat si la fonction n'est pas nulle), il utilise le cadre de pile de l'appel en cours au lieu d'en configurer un nouveau et effectue un "saut" à la place. d'un "appel" (donc l'espace de pile utilisé reste constant). En bref, cela fait environ 90% du chemin vers la transformation de la récursivité de queue en itération. Il pourrait gérer ces milliards d'ints sans déborder la pile. (Il finirait toujours par manquer de mémoire, mais assembler une liste d'un milliard d'ints va de toute façon vous gâcher en termes de mémoire sur un système 32 bits.)
Java ne fait pas cela, dans la plupart des cas. (Cela dépend du compilateur et de l'exécution, mais l'implémentation d'Oracle ne le fait pas.) Chaque appel à une fonction récursive consomme la mémoire d'une trame de pile. Utilisez trop et vous obtenez un débordement de pile. Débordement de la pile, mais garantit la mort du programme. Nous devons donc nous assurer de ne pas le faire.
Un semi-contournement ... évaluation paresseuse. Nous avons toujours les limites de la pile, mais elles peuvent être liées à des facteurs sur lesquels nous avons plus de contrôle. Nous n'avons pas à calculer un million d'ints juste pour en retourner 25. :)
Construisons-nous donc une infrastructure d'évaluation paresseuse. (Ce code a été testé il y a quelque temps, mais je l'ai beaucoup modifié depuis; lisez l'idée, pas les erreurs de syntaxe. :))
(Gardez à l'esprit que si cela était réellement viable en Java, du code au moins un peu comme celui-ci ferait déjà partie de l'API.)
Maintenant, avec une infrastructure en place, il est plutôt trivial d'écrire du code qui n'a pas besoin de variables mutables et qui est au moins stable pour de plus petites quantités d'entrée.
Cela fonctionne principalement, mais il est toujours quelque peu sujet à des débordements de pile. Essayer
take
2 milliards d'ints et agir sur eux. : P Il lèvera éventuellement une exception, au moins jusqu'à ce que 64+ Go de RAM deviennent standard. Le problème est que la quantité de mémoire d'un programme réservée à sa pile n'est pas si grande. Il se situe généralement entre 1 et 8 Mio. (Vous pouvez demander plus, mais il n'a pas d' importance tant que ça combien vous demandez - vous appeleztake(1000000000, someInfiniteSequence)
, vous allez . Obtenir une exception) Heureusement, avec évaluation paresseuse, le point faible est dans une zone que nous pouvons mieux contrôler . Nous devons juste faire attention à combien noustake()
.Il y aura toujours beaucoup de problèmes à l'échelle, car notre utilisation de la pile augmente de manière linéaire. Chaque appel gère un élément et transmet le reste à un autre appel. Maintenant que j'y pense, cependant, il y a une astuce que nous pouvons tirer qui pourrait nous gagner beaucoup plus de marge: transformer la chaîne d'appels en un arbre d'appels. Considérez quelque chose de plus comme ceci:
workWith
divise essentiellement le travail en deux moitiés et attribue chaque moitié à un autre appel à lui-même. Étant donné que chaque appel réduit la taille de la liste de travail de moitié plutôt que d'un, cela doit être mis à l'échelle logarithmiquement plutôt que linéairement.Le problème est que cette fonction veut une entrée - et avec une liste chaînée, obtenir la longueur nécessite de parcourir toute la liste. C'est facile à résoudre, cependant; ne vous souciez simplement pas du nombre d'entrées. :) Le code ci-dessus fonctionnerait avec quelque chose comme
Integer.MAX_VALUE
le nombre, car une valeur nulle arrête le traitement de toute façon. Le décompte est principalement là, nous avons donc un cas de base solide. Si vous prévoyez d'avoir plus deInteger.MAX_VALUE
entrées dans une liste, vous pouvez vérifierworkWith
la valeur de retour de - elle doit être nulle à la fin. Sinon, récusez.Gardez à l'esprit que cela touche autant d'éléments que vous le dites. Ce n'est pas paresseux; il fait son truc immédiatement. Vous ne voulez le faire que pour des actions - c'est-à-dire des trucs dont le seul but est de s'appliquer à chaque élément d'une liste. Comme j'y réfléchis en ce moment, il me semble que les séquences seraient beaucoup moins compliquées si elles étaient linéaires; ne devrait pas être un problème, car les séquences ne s'appellent pas de toute façon - elles créent simplement des objets qui les appellent à nouveau.
la source
J'ai déjà essayé de créer un interpréteur pour un langage de type lisp en Java (il y a quelques années et tout le code a été perdu comme dans CVS à sourceforge), et les itérateurs d'utilisation Java sont un peu verbeux pour la programmation fonctionnelle sur les listes.
Voici quelque chose basé sur une interface de séquence, qui a juste les deux opérations dont vous avez besoin pour obtenir la valeur actuelle et obtenir la séquence commençant à l'élément suivant. Ceux-ci sont nommés tête et queue d'après les fonctions du schéma.
Il est important d'utiliser quelque chose comme les interfaces
Seq
ouIterator
car cela signifie que la liste est créée paresseusement. L'Iterator
interface ne peut pas être un objet immuable, elle est donc moins adaptée à la programmation fonctionnelle - si vous ne pouvez pas dire si la valeur que vous passez dans une fonction a été modifiée par elle, vous perdez l'un des principaux avantages de la programmation fonctionnelle.Évidemment,
integers
devrait être une liste de tous les entiers, alors j'ai commencé à zéro et retourné alternativement des positifs et des négatifs.Il existe deux versions de carrés - l'une créant une séquence personnalisée, l'autre utilisant
map
qui prend une `` fonction '' - Java 7 n'a pas de lambdas, j'ai donc utilisé une interface - et l'applique à chaque élément de la séquence tour à tour.Le but de la
square ( int x )
fonction est seulement de supprimer le besoin d'appelerhead()
deux fois - normalement j'aurais fait cela en mettant la valeur dans une variable finale, mais l'ajout de cette fonction signifie qu'il n'y a pas de variables dans le programme, seulement des paramètres de fonction.La verbosité de Java pour ce type de programmation m'a amené à écrire la deuxième version de mon interprète en C99 à la place.
la source
En théorie, vous pouvez implémenter à peu près n'importe quoi en Java en utilisant uniquement la récursivité et aucune variable mutable.
En pratique:
Le langage Java n'est pas conçu pour cela. De nombreuses constructions sont conçues pour la mutation et sont difficiles à utiliser sans elle. (Par exemple, vous ne pouvez pas initialiser un tableau Java de longueur variable sans mutation.)
Idem pour les bibliothèques. Et si vous vous limitez aux classes de bibliothèque qui n'utilisent pas de mutation sous le couvercle, c'est encore plus difficile. (Vous ne pouvez même pas utiliser String ... regardez comment
hashcode
est implémenté.)Les implémentations Java standard ne prennent pas en charge l'optimisation des appels de queue. Cela signifie que les versions récursives des algorithmes ont tendance à être "gourmandes" en espace de pile. Et comme les piles de threads Java ne se développent pas, vous devez préallouer de grandes piles ... ou risquer
StackOverflowError
.Combinez ces trois choses, et Java n'est pas vraiment une option viable pour écrire des programmes utiles (c'est-à-dire non triviaux) sans variables mutables.
(Mais bon, c'est OK. Il existe d'autres langages de programmation disponibles pour la JVM, dont certains prennent en charge la programmation fonctionnelle.)
la source
Comme nous recherchons un exemple des concepts, je dirais que laissons de côté Java et cherchons un paramètre différent mais familier dans lequel trouver une version familière des concepts. Les canaux UNIX sont assez similaires au chaînage de fonctions paresseuses.
Sous Linux, cela signifie, donnez-moi des octets dont chacun est composé de faux plutôt que de vrais bits, jusqu'à ce que je perde l'appétit; changer chacun de ces octets en un caractère de nouvelle ligne; numéroter chaque ligne ainsi créée; et produire le carré de ce nombre. De plus j'ai de l'appétit pour 25 lignes et pas plus.
Je prétends qu'un programmeur ne serait pas mal avisé d'écrire un pipeline Linux de cette manière. C'est un script shell Linux relativement normal.
Je prétends qu'un programmeur serait mal avisé d'essayer d'écrire la même chose de façon similaire en Java. La raison en est la maintenance des logiciels en tant que facteur majeur du coût à vie des projets logiciels. Nous ne voulons pas embrouiller le prochain programmeur en présentant ce qui est ostensiblement un programme Java, mais qui est en fait écrit en effet dans un langage personnalisé unique en dupliquant de manière élaborée des fonctionnalités qui existent déjà dans la plate-forme Java.
D'un autre côté, je prétends que le prochain programmeur pourrait être plus tolérant si certains de nos packages "Java" sont en fait des packages Java Virtual Machine écrits dans l'un des langages fonctionnels ou objet / fonctionnels tels que Clojure et Scala. Celles-ci sont conçues pour être codées en enchaînant des fonctions et être appelées à partir de Java de la manière habituelle des appels de méthode Java.
Là encore, il peut toujours être une bonne idée pour un programmeur Java de s'inspirer de la programmation fonctionnelle, par endroits.
Récemment, ma technique préférée [était] d'utiliser une variable de retour immuable et non initialisée et une seule sortie de sorte que, comme le font certains compilateurs de langage fonctionnel, Java vérifie que, quoi qu'il arrive dans le corps de la fonction, je fournis toujours une et une seule valeur de retour. Exemple:la source
int result = -n; if (n < 1) { result = 0 } return result;
qu'il compile très bien et que le compilateur n'a aucune idée si vous aviez l'intention de le rendre équivalent à la fonction dans mon exemple. Peut-être que cet exemple est trop simple pour rendre la technique utile, mais dans une fonction avec beaucoup de branches, je pense qu'il est agréable de préciser que le résultat est attribué exactement une fois quel que soit le chemin suivi.if (n < 1) return 0; else return -n;
, cependant, vous vous retrouvez sans problème ... et c'est d'ailleurs plus simple. :) me semble que dans ce cas, la règle « un retour » contribue effectivement à cause de la question de ne pas savoir quand votre valeur de retour a été fixé; sinon, vous pourriez simplement la renvoyer et Java serait plus en mesure de déterminer quand d'autres chemins pourraient ne pas retourner une valeur, car vous ne dissociez plus le calcul de la valeur du retour réel de celle-ci.if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;
.Le moyen le plus simple de le découvrir serait de fournir ce qui suit au compilateur Frege et de regarder le code java généré:
la source