Comment écrire des programmes Java utiles sans utiliser de variables mutables

12

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.

minusSeven
la source
19
Votre exemple fonctionnel utilise des variables à l'intérieur, mais le langage fait tout cela en arrière-plan. Vous avez effectivement délégué les parties désagréables à quelqu'un qui, selon vous, l'a fait correctement.
Blrfl
12
@Blrfl: L'argument "dans les coulisses" tue tous les débats basés sur la langue, car chaque morceau de code est finalement traduit en code machine x86. Le code x86 n'est pas orienté objet, ni procédural, ni fonctionnel, ni rien, mais ces catégories sont des balises précieuses pour les langages de programmation. Regardez la langue, pas l'implémentation.
thiton
10
@thiton En désaccord. Ce que Blrfl dit, c'est que ces fonctions utilisent probablement des variables écrites dans le même langage de programmation . Pas besoin d'aller à bas niveau ici. L'extrait utilise simplement des fonctions de bibliothèque. Vous pouvez facilement écrire le même code en Java, voir: squaresOf(integers()).take(25)(l'écriture de ces fonctions reste un exercice pour le lecteur; la difficulté réside dans l'ensemble infini pour integers(), mais c'est un problème pour Java en raison de son évaluation avide, rien à voir avec variables)
Andres F.
6
Cette citation est déroutante et trompeuse, il n'y a pas de magie, juste du sucre syntaxique .
yannis
2
@thiton Je vous suggère d'en savoir plus sur les langages FP, mais néanmoins, l'extrait de code ne prend pas en charge (ou rejette) l'affirmation selon laquelle les "variables" ne sont pas nécessaires (par lesquelles je suppose que vous voulez dire "variables mutables", parce que l'autre genre est commun en PF). L'extrait montre simplement les fonctions de bibliothèque qui auraient également pu être implémentées en Java, sauf les problèmes paresseux / impatients qui sont hors sujet ici.
Andres F.

Réponses:

31

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:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

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

  1. lier une valeur à un nom: final int MAX_SIZE = 100;

  2. 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, defineitions 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 sont take, squares-ofet integerssi pas les variables?

De plus, l'exemple n'a pas de sens. Il ne montre pas les mises en œuvre de take, squares-ofou integers. 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:

Ce document est une tentative de démontrer à la plus grande communauté de programmeurs (non fonctionnels) la signification de la programmation fonctionnelle, et aussi d'aider les programmeurs fonctionnels à exploiter pleinement ses avantages en expliquant clairement quels sont ces avantages.

[...]

Nous soutiendrons dans la suite de cet article que les langages fonctionnels fournissent deux nouveaux types de colle très importants. Nous donnerons quelques exemples de programmes qui peuvent être modularisés de nouvelles manières et peuvent ainsi être simplifiés. C'est la clé de la puissance de la programmation fonctionnelle - elle permet une modularisation améliorée. C'est aussi le but pour lequel les programmeurs fonctionnels doivent tendre - des modules plus petits et plus simples et plus généraux, collés avec les nouvelles colles que nous décrirons.


la source
10
+1 pour "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 plus dans le but de choquer et d'offenser plutôt que d'enseigner les concepts et les fondamentaux."
3
La moitié de la raison pour laquelle les gens ne font pas de PF est parce qu'ils n'en entendent / apprennent rien à l'université, et l'autre moitié parce que lorsqu'ils y regardent, ils trouvent des articles qui les laissent tous les deux mal informés et pensent que tout cela est pour certains fantaisistes jouer plutôt que d'être une approche raisonnée et réfléchie avec des avantages. +1 pour avoir fourni de meilleures sources d'information
Jimmy Hoffa
3
Mettez votre réponse à la question tout en haut si vous le souhaitez afin qu'elle soit plus directe à la question et peut-être que cette question restera ouverte alors (avec une réponse directe axée sur la question)
Jimmy Hoffa
2
Désolé de tergiverser, mais je ne comprends pas pourquoi vous avez choisi ce code haskell. J'ai lu LYAH et votre exemple est difficile à comprendre pour moi. Je ne vois pas non plus la relation avec la question d'origine. Pourquoi n'avez-vous pas simplement utilisé take 25 (map (^2) [1..])comme exemple?
Daniel Kaplan
2
@tieTYT bonne question - merci de l'avoir signalé. La raison pour laquelle j'ai utilisé cet exemple est parce qu'il montre comment générer une liste de nombres en utilisant la récursivité et en évitant les variables mutables. Mon intention était que l'OP voie ce code et réfléchisse à la façon de faire quelque chose de similaire en Java. Pour résoudre votre extrait de code, qu'est-ce que c'est [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 la Enumclasse (dont cette syntaxe nécessite) sont également utiles, mais étaient trop paresseuses pour les trouver. Ainsi, unfoldr. :)
27

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:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

et ne vous laissez pas berner pour l'écrire de manière plus complexe en raison d'une haine des variables.

thiton
la source
5
"Haine de variables"? Ooookay ... Qu'avez-vous lu sur la programmation fonctionnelle? Quelles langues avez-vous essayé? Quels tutoriels?
Andres F.15
8
@AndresF .: Plus de deux ans de cours à Haskell. Je ne dis pas que FP est mauvais. Cependant, il y a une tendance dans de nombreuses discussions FP-vs-IP (comme l'article lié) à condamner l'utilisation d'entités nommées réaffectables (variables AKA), et à condamner sans raison ni données. La condamnation déraisonnable est de la haine dans mon livre. Et la haine fait un très mauvais code.
thiton
10
« Haine des variables » est causale simplisme en.wikipedia.org/wiki/Fallacy_of_the_single_cause il y a beaucoup d' avantages à la programmation sans état qui pourrait même être eu en Java, même si je suis d' accord avec votre réponse que Java le coût serait trop élevé en complexité le programme et étant non idiomatique. Je n'irais toujours pas autour de l'idée que les programmes apatrides sont bons et avec état sont mauvais, car il s'agit d'une réponse émotionnelle plutôt que d'une position raisonnée et bien réfléchie à laquelle les gens sont parvenus en raison de l'expérience.
Jimmy Hoffa
2
En ligne avec ce que dit @JimmyHoffa, je vous renvoie à John Carmack sur le thème de la programmation de style fonctionnel dans les langages impératifs (C ++ dans son cas) ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Steven Evers
5
Une condamnation déraisonnable n'est pas de la haine, et éviter un état mutable n'est pas déraisonnable.
Michael Shaw
21

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:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}
FrustratedWithFormsDesigner
la source
3
+1 pour avoir tenté de répondre à la question avec un exemple Java.
KChaloux
Je voterais contre ceci pour la présentation du style de golf de code (voir l' avis de Mod ) mais je ne peux pas m'obliger à appuyer sur la flèche vers le bas parce que ce code correspond parfaitement aux déclarations faites dans ma réponse préférée : "1) l'absence de mutation, 2) l'utilisation de récursivité, et 3) le manque de boucles "
gnat
3
@gnat: Cette réponse a été publiée avant l'avis de modification. Je n'allais pas pour le grand style, j'allais pour la simplicité et la satisfaction de la question originale de l'OP; pour illustrer qu'il est possible de faire de telles choses en Java.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner sûr; cela ne m'empêcherait pas de faire du DV (puisque vous êtes censé être capable d' éditer pour vous conformer) - c'est la correspondance étonnamment parfaite qui a fait la magie. Bien joué, vraiment bien fait, assez éducatif - merci
moucher
16

Dans votre exemple fonctionnel, nous ne voyons pas comment les fonctions squares-ofet takesont 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 ...

squares_of(integers).take(25);

ce qui n'est pas si différent.

Martin
la source
6
Nitpick: squares-ofn'est pas un nom valide en Java (l' squares_ofest cependant). Mais sinon, bon point qui montre que l'exemple de l'article est mauvais.
Je soupçonne que l'article integergénère paresseusement des entiers, et la takefonction choisit 25 des squared-ofnombres integer. En bref, vous devriez avoir une integerfonction pour produire paresseusement des entiers à l'infini.
OnesimusUnbound
C'est un peu insensé d'appeler quelque chose comme (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 que integerc'est une variable qui est liée à un nombre infini de nombres.
Ingo
6

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 Takeconstructeur peut être arbitrairement élevé.

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Ou avec des méthodes d'usine chaînables:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

SquaresOf, Takeet IntegersétendreNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}
artistoex
la source
1
Cela montre la supériorité du paradigme OO sur celui fonctionnel; avec une conception OO appropriée, vous pouvez imiter le paradigme fonctionnel, mais vous ne pouvez pas imiter le paradigme OO dans un style fonctionnel.
m3th0dman
3
@ m3th0dman: Avec une conception OO appropriée, vous pouvez éventuellement imiter à moitié la FP, tout comme n'importe quelle langue qui a des chaînes, des listes et / ou des dictionnaires pourrait imiter à moitié la OO. L'équivalence de Turing des langages à usage général signifie qu'avec suffisamment d'efforts, n'importe quel langage peut simuler les caractéristiques de n'importe quel autre.
cHao
Notez que les itérateurs de style Java comme dans while (test.hasNext()) System.out.println(test.next())seraient un non-non dans FP; les itérateurs sont intrinsèquement mutables.
cHao
1
@cHao Je ne crois guère qu'une véritable encapsulation ou polymorphisme puisse être imité; aussi Java (dans cet exemple) ne peut pas vraiment imiter un langage fonctionnel en raison de l'évaluation stricte et désireuse. Je pense également que les itérateurs peuvent être écrits de manière récursive.
m3th0dman
@ m3th0dman: Le polymorphisme ne serait pas du tout difficile à imiter; même C et le langage d'assemblage peuvent le faire. Faites simplement de la méthode un champ dans l'objet ou un descripteur de classe / vtable. Et l'encapsulation au sens caché des données n'est pas strictement nécessaire; la moitié des langues ne le fournissent pas, lorsque votre objet est immuable, peu importe que les gens puissent voir ses tripes de toute façon. tout ce qui est nécessaire est un habillage de données , que les champs de méthode susmentionnés pourraient facilement fournir.
cHao
6

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.

  • Les langages impératifs dépendent presque toujours de variables mutables d'une certaine sorte. Ils ont tendance à privilégier l'itération à la récursivité, par exemple, et presque toutes les constructions itératives - même while (true)et for (;;)! - dépendent totalement d'une variable qui change quelque part d'itération en itération.
  • Les langages orientés objet envisagent à peu près chaque programme comme un graphique d'objets qui s'envoient des messages et, dans presque tous les cas, répondent à ces messages en mutant quelque chose.

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...

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

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:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

Bien que nous n'ayons besoin que de 25 pouces pour le résultat, squares_ofje ne le sais pas. Il va retourner le carré de chaque nombre integers. 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. :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(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.

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

Cela fonctionne principalement, mais il est toujours quelque peu sujet à des débordements de pile. Essayer take2 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 appelez take(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 nous take().

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:

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

workWithdivise 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_VALUEle 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 de Integer.MAX_VALUEentrées dans une liste, vous pouvez vérifier workWithla 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.

cHao
la source
3

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 Seqou Iteratorcar cela signifie que la liste est créée paresseusement. L' Iteratorinterface 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, integersdevrait ê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 mapqui 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'appeler head()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.

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}
Pete Kirkham
la source
3

Comment écrire des programmes Java utiles sans utiliser de variables mutables.

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 hashcodeest 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.)

Stephen C
la source
2

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.

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

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:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}

minopret
la source
Je suis certain qu'à 70%, Java vérifie déjà la valeur de retour de toute façon. Vous devriez obtenir une erreur concernant une «instruction de retour manquante» si le contrôle peut tomber de la fin d'une fonction non vide.
cHao
Mon point: si vous le codez pendant 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.
minopret
Si vous dites 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.
cHao
Ou, pour l'exemple de votre réponse, if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao
Je viens de décider que je ne veux plus passer de moments de ma vie à défendre la règle OnlyOneReturn en Java. C'est parti. Quand et si je pense à une pratique de codage Java que j'ai envie de défendre et qui est influencée par des pratiques de programmation fonctionnelles, je remplacerai cet exemple. Jusque-là, pas d'exemple.
minopret
0

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é:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x
Ingo
la source
Après quelques jours, j'ai retrouvé mes pensées revenant à cette réponse. Après tout, une partie de ma suggestion était d'implémenter les parties de programmation fonctionnelle dans Scala. Si nous envisageons d'appliquer Scala dans les endroits où nous pensions vraiment à Haskell (et je pense que je ne suis pas le seul blog.zlemma.com/2013/02/20/… ), ne devrions-nous pas au moins envisager Frege?
minopret
@minopret C'est en effet le créneau que Frege est en train de tragétiser - des gens qui ont appris à connaître et à aimer Haskell et qui ont pourtant besoin de la JVM. Je suis convaincu qu'un jour, Frege sera suffisamment mûr pour avoir au moins une considération sérieuse.
Ingo