Définition de la programmation fonctionnelle
L'introduction de The Joy of Clojure dit ce qui suit:
La programmation fonctionnelle est l'un de ces termes informatiques qui a une définition amorphe. Si vous demandez à 100 programmeurs leur définition, vous recevrez probablement 100 réponses différentes ...
La programmation fonctionnelle concerne et facilite l'application et la composition des fonctions ... Pour qu'un langage soit considéré comme fonctionnel, sa notion de fonction doit être de premier ordre. Les fonctions de première classe peuvent être stockées, transmises et renvoyées comme n'importe quelle autre donnée. Au-delà de ce concept de base, [les définitions de la PF peuvent inclure] la pureté, l'immuabilité, la récursivité, la paresse et la transparence référentielle.
Programmation dans Scala 2nd Edition p. 10 a la définition suivante:
La programmation fonctionnelle est guidée par deux idées principales. La première idée est que les fonctions sont des valeurs de première classe ... Vous pouvez passer des fonctions comme arguments à d'autres fonctions, les renvoyer comme résultats de fonctions ou les stocker dans des variables ...
La deuxième idée principale de la programmation fonctionnelle est que les opérations d'un programme doivent mapper les valeurs d'entrée aux valeurs de sortie plutôt que de modifier les données en place.
Si nous acceptons la première définition, alors la seule chose que vous devez faire pour rendre votre code "fonctionnel" est de retourner vos boucles à l'envers. La deuxième définition inclut l'immuabilité.
Fonctions de première classe
Imaginez que vous obtenez actuellement une liste de passagers de votre objet Bus et que vous itérez dessus en diminuant le compte bancaire de chaque passager du montant du tarif du bus. La manière fonctionnelle d'effectuer cette même action serait d'avoir une méthode sur Bus, peut-être appelée forEachPassenger qui prend en fonction un argument. Ensuite, Bus itérerait sur ses passagers, mais cela est préférable et votre code client qui facture le prix du trajet serait mis en fonction et transmis à forEachPassenger. Voila! Vous utilisez une programmation fonctionnelle.
Impératif:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Fonctionnel (en utilisant une fonction anonyme ou "lambda" dans Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Version Scala plus sucrée:
myBus = myBus.forEachPassenger(_.debit(fare))
Fonctions non de première classe
Si votre langue ne prend pas en charge les fonctions de première classe, cela peut devenir très moche. Dans Java 7 ou version antérieure, vous devez fournir une interface "Functional Object" comme celle-ci:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
Ensuite, la classe Bus fournit un itérateur interne:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Enfin, vous passez un objet fonction anonyme au bus:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
Java 8 permet aux variables locales d'être capturées dans le cadre d'une fonction anonyme, mais dans les versions antérieures, ces varibales doivent être déclarées finales. Pour contourner ce problème, vous devrez peut-être créer une classe wrapper MutableReference. Voici une classe spécifique à un entier qui vous permet d'ajouter un compteur de boucles au code ci-dessus:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
Même avec cette laideur, il est parfois avantageux d'éliminer la logique compliquée et répétée des boucles réparties dans votre programme en fournissant un itérateur interne.
Cette laideur a été corrigée dans Java 8, mais la gestion des exceptions vérifiées dans une fonction de première classe est toujours vraiment laide et Java porte toujours l'hypothèse de la mutabilité dans toutes ses collections. Ce qui nous amène aux autres objectifs souvent associés à la PF:
Immutabilité
L'article 13 de Josh Bloch est «Préférer l'immuabilité». Malgré le fait que les ordures parlent le contraire, la POO peut être effectuée avec des objets immuables, ce qui le rend beaucoup mieux. Par exemple, String en Java est immuable. StringBuffer, OTOH doit être modifiable pour construire une chaîne immuable. Certaines tâches, comme travailler avec des tampons, nécessitent intrinsèquement une mutabilité.
Pureté
Chaque fonction doit au moins être mémorisable - si vous lui donnez les mêmes paramètres d'entrée (et qu'elle ne devrait avoir aucune entrée en dehors de ses arguments réels), elle devrait produire la même sortie à chaque fois sans provoquer "d'effets secondaires" comme changer l'état global, effectuer I / O, ou lever des exceptions.
Il a été dit que dans la programmation fonctionnelle, "un peu de mal est généralement nécessaire pour faire le travail". La pureté à 100% n'est généralement pas l'objectif. La minimisation des effets secondaires est.
Conclusion
Vraiment, de toutes les idées ci-dessus, l'immuabilité a été la plus grande victoire unique en termes d'applications pratiques pour simplifier mon code - que ce soit OOP ou FP. La transmission de fonctions aux itérateurs est la deuxième plus grande victoire. La documentation Java 8 Lambdas explique le mieux pourquoi. La récursivité est idéale pour traiter les arbres. La paresse vous permet de travailler avec des collections infinies.
Si vous aimez la JVM, je vous recommande de jeter un œil à Scala et Clojure. Les deux sont des interprétations perspicaces de la programmation fonctionnelle. Scala est de type sécurisé avec une syntaxe quelque peu similaire à C, bien qu'il ait vraiment autant de syntaxe en commun avec Haskell qu'avec C. Clojure n'est pas de type sécurisé et c'est un Lisp. J'ai récemment publié une comparaison de Java, Scala et Clojure en ce qui concerne un problème de refactorisation spécifique. La comparaison de Logan Campbell avec Game of Life inclut également Haskell et Clojure.
PS
Jimmy Hoffa a souligné que ma classe de bus est modifiable. Plutôt que de corriger l'original, je pense que cela démontrera exactement le type de refactorisation de cette question. Cela peut être résolu en faisant de chaque méthode sur le bus une usine pour produire un nouveau bus, chaque méthode sur le passager une usine pour produire un nouveau passager. J'ai donc ajouté un type de retour à tout ce qui signifie que je vais copier java.util.function.Function de Java 8 au lieu de l'interface consommateur:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Puis en bus:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
Enfin, l'objet fonction anonyme renvoie l'état modifié des choses (un nouveau bus avec de nouveaux passagers). Cela suppose que p.debit () retourne maintenant un nouveau passager immuable avec moins d'argent que l'original:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
J'espère que vous pouvez maintenant prendre votre propre décision sur la façon dont vous voulez rendre votre langage impératif fonctionnel, et décider s'il serait préférable de repenser votre projet en utilisant un langage fonctionnel. Dans Scala ou Clojure, les collections et autres API sont conçues pour faciliter la programmation fonctionnelle. Les deux ont une très bonne interopérabilité Java, vous pouvez donc mélanger et faire correspondre les langues. En fait, pour l'interopérabilité Java, Scala compile ses fonctions de première classe en classes anonymes qui sont presque compatibles avec les interfaces fonctionnelles Java 8. Vous pouvez lire les détails dans Scala in Depth sect. 1.3.2 .