web-dev-qa-db-fra.com

Aide à la compréhension d'un objet fonction ou d'un foncteur dans Java

Quelqu'un peut-il expliquer ce qu'est un foncteur et fournir un exemple simple?

26
jlss4e

Un objet fonction est juste cela. Quelque chose qui est à la fois un objet et une fonction.

De plus: appeler un objet fonction un "foncteur" est un abus grave du terme: un autre type de "foncteurs" est un concept central en mathématiques, et qui a un rôle direct dans l'informatique (voir "Haskell Functors"). Le terme est également utilisé d'une manière légèrement différente en ML, donc à moins que vous n'implémentiez l'un de ces concepts en Java (que vous pouvez!) Veuillez cesser d'utiliser cette terminologie. Cela rend les choses simples compliquées .

Retour à la réponse: Java n'a pas de "fonctions de première classe" c'est-à-dire que vous ne pouvez pas passer une fonction comme argument à une fonction. Ceci est vrai à plusieurs niveaux, syntaxiquement, dans la représentation du code octet, et en ce que le système de type n'a pas le "constructeur de fonction"

En d'autres termes, vous ne pouvez pas écrire quelque chose comme ceci:

 public static void tentimes(Function f){
    for(int i = 0; i < 10; i++)
       f();
 }
 ...
 public static void main{
    ...
    tentimes(System.out.println("hello"));
    ...
 }

C'est vraiment ennuyeux, car nous voulons pouvoir faire des choses comme avoir des bibliothèques d'interface utilisateur graphique où vous pouvez associer une fonction de "rappel" en cliquant sur un bouton.

Alors que faisons-nous?

Eh bien, la solution générale (discutée par les autres affiches) est de définir une interface avec une seule méthode que nous pouvons appeler. Par exemple, Java utilise une interface appelée Runnable pour ce genre de choses tout le temps, cela ressemble à:

public interface Runnable{
    public void run();
}

maintenant, nous pouvons réécrire mon exemple d'en haut:

public static void tentimes(Runnable r){
    for(int i = 0; i < 10; i++)
       r.run();
}
...
public class PrintHello implements Runnable{
    public void run{
       System.out.println("hello")
    }
}
---
public static void main{
    ...
    tentimes(new PrintHello());
    ...
 }

De toute évidence, cet exemple est artificiel. Nous pourrions rendre ce code un peu plus agréable en utilisant des classes internes anonymes, mais cela donne l'idée générale.

Voici où cela tombe en panne: Runnable n'est utilisable que pour les fonctions qui ne prennent aucun argument et ne renvoient rien d'utile, vous finissez donc par définir une nouvelle interface pour chaque travail. Par exemple, l'interface Comparator dans la réponse de Mohammad Faisal. Fournir une approche plus générale, et qui prend la syntaxe, est un objectif majeur pour Java 8 (la prochaine version de Java), et a été fortement poussé à être inclus dans Java 7. Ceci est appelé un "lambda" après le mécanisme d'abstraction de fonction dans le Lambda Calculus. Lambda Calculus est à la fois (peut-être) le langage de programmation le plus ancien, et la base théorique d'une grande partie de l'informatique. Quand Alonzo Church (un des principaux fondateurs de l'informatique) l'a inventé, il a utilisé la lettre grecque lambda pour les fonctions, d'où le nom.

Autres langages, y compris le langage fonctionnel (LISP, ML, Haskell, Erlang, etc.), la plupart des principaux langages dynamiques (Python, Ruby, JavaScript, etc.) et les autres langages d'application (C #, Scala, Go, D, etc.) prendre en charge une certaine forme de "Lambda Literal". Même C++ les a maintenant (depuis C++ 11), bien que dans ce cas, ils soient un peu plus compliqués car C++ manque de gestion automatique de la mémoire et ne sauvera pas votre cadre de pile pour vous.

39
Philip JF

Un foncteur est un objet qui est une fonction.

Java n'en a pas, car les fonctions ne sont pas des objets de première classe en Java.

Mais vous pouvez les rapprocher avec des interfaces, quelque chose comme un objet Command:

public interface Command {
    void execute(Object [] parameters); 
}

Mis à jour le 18 mars 2017:

Depuis que j'ai écrit pour la première fois, JDK 8 a ajouté des lambdas. Le package Java.util.function possède plusieurs interfaces utiles.

14
duffymo

Des vérifications à chaque fois, aux foncteurs, à Java 8 Lambdas (en quelque sorte)

Le problème

Prenez cet exemple de classe, qui adapte an Appendable dans un Writer :

   import  Java.io.Closeable;
   import  Java.io.Flushable;
   import  Java.io.IOException;
   import  Java.io.Writer;
   import  Java.util.Objects;
/**
   <P>{@code Java WriterForAppendableWChecksInFunc}</P>
 **/
public class WriterForAppendableWChecksInFunc extends Writer  {
   private final Appendable apbl;
   public WriterForAppendableWChecksInFunc(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;
   }

      //Required functions, but not relevant to this post...START
         public void write(char[] a_c, int i_ndexStart, int i_ndexEndX) throws IOException {
         public Writer append(char c_c) throws IOException {
         public Writer append(CharSequence text) throws IOException {
         public Writer append(CharSequence text, int i_ndexStart, int i_ndexEndX) throws IOException  {
      //Required functions, but not relevant to this post...END

   public void flush() throws IOException {
      if(apbl instanceof Flushable)  {
         ((Flushable)apbl).flush();
      }
   }
   public void close() throws IOException {
      flush();
      if(apbl instanceof Closeable)  {
         ((Closeable)apbl).close();
      }
   }
}

Tous les Appendable ne sont pas Flushable ou Closeable, mais ceux qui le sont, doivent également être fermés et vidés. Le type réel de l'objet Appendable doit donc être vérifié dans chaque appel à flush() et close() et, lorsqu'il s'agit bien de ce type, il est casté et la fonction est appelé.

Certes, ce n'est pas le meilleur exemple, car close() n'est appelé qu'une seule fois par instance, et flush() n'est pas nécessairement appelé aussi souvent. De plus, instanceof, bien que réfléchissant, n'est pas trop mal compte tenu de cet exemple d'utilisation particulier. Pourtant, le concept de avoir à vérifier quelque chose à chaque fois que vous devez faire autre chose est réel, et éviter ces vérifications "à chaque fois", quand cela compte vraiment, offre des avantages significatifs.

Déplacez tous les chèques "lourds" vers le constructeur

Alors par où commencer? Comment éviter ces contrôles sans compromettre votre code?

Dans notre exemple, l'étape la plus simple consiste à déplacer toutes les vérifications instanceof vers le constructeur.

public class WriterForAppendableWChecksInCnstr extends Writer  {
   private final Appendable apbl;
   private final boolean isFlshbl;
   private final boolean isClsbl;
   public WriterForAppendableWChecksInCnstr(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;
      isFlshbl = (apbl instanceof Flushable);
      isClsbl = (apbl instanceof Closeable);
   }

         //write and append functions go here...

   public void flush() throws IOException {
      if(isFlshbl)  {
         ((Flushable)apbl).flush();
      }
   }
   public void close() throws IOException {
      flush();
      if(isClsbl)  {
         ((Closeable)apbl).close();
      }
   }
}

Maintenant que ces vérifications "intensives" ne sont effectuées qu'une seule fois, seules les vérifications booléennes doivent être effectuées par flush() et close(). Bien qu'il s'agisse certainement d'une amélioration, comment ces contrôles en fonctionnement peuvent-ils être entièrement éliminés?

Si seulement vous pouviez en quelque sorte définir un fonction qui pourrait être stocké par la classe, puis utilisé par flush() et close()...

public class WriterForAppendableWChecksInCnstr extends Writer  {
   private final Appendable apbl;
   private final FlushableFunction flshblFunc;  //If only!
   private final CloseableFunction clsblFunc;   //If only!
   public WriterForAppendableWChecksInCnstr(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;

      if(apbl instanceof Flushable)  {
         flshblFunc = //The flushable function
      }  else  {
         flshblFunc = //A do-nothing function
      }
      if(apbl instanceof Closeable)  {
         clsblFunc = //The closeable function
      }  else  {
         clsblFunc = //A do-nothing function
      }
   }

          //write and append functions go here...

   public void flush() throws IOException {
      flshblFunc();                             //If only!
   }
   public void close() throws IOException {
      flush();
      clsblFunc();                              //If only!
   }
}

Mais passer des fonctions n'est pas possible ... du moins pas avant Java 8 Lambdas . Alors, comment faites-vous dans la pré-8 versions de Java?

Foncteurs

Avec un Functor . Un Functor est fondamentalement un Lambda, mais celui qui est enveloppé dans un objet. Bien que les fonctions ne puissent pas être transmises à d'autres fonctions en tant que paramètres, les objets peuvent. Donc, essentiellement, les Functors et Lambdas sont un moyen de passer autour des fonctions.

Alors, comment pouvons-nous implémenter un Functor dans notre écrivain-adaptateur? Ce que nous savons, c'est que close() et flush() ne sont utiles qu'avec les objets Closeable et Flushable. Et que certains Appendable sont Flushable, certains Closeable, certains non, certains les deux.

Par conséquent, nous pouvons stocker un objet Flushable et Closeable en haut de la classe:

public class WriterForAppendable extends Writer  {
   private final Appendable apbl;
   private final Flushable  flshbl;
   private final Closeable  clsbl;
   public WriterForAppendable(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }

      //Avoids instanceof at every call to flush() and close()

      if(apbl instanceof Flushable)  {
         flshbl = apbl;              //This Appendable *is* a Flushable
      }  else  {
         flshbl = //??????           //But what goes here????
      }

      if(apbl instanceof Closeable)  {
         clsbl = apbl;               //This Appendable *is* a Closeable
      }  else  {
         clsbl = //??????            //And here????
      }

      this.apbl = apbl;
   }

          //write and append functions go here...

   public void flush() throws IOException {
      flshbl.flush();
   }
   public void close() throws IOException {
      flush();
      clsbl.close();
   }
}

Les contrôles "à chaque fois" ont maintenant été supprimés. Mais quand le Appendable est ne pas un Flushable ou ne pas a Closeable, que doit-on stocker?

Ne rien faire Functors

Un Functor ne rien faire ...

class CloseableDoesNothing implements Closeable  {
   public void close() throws IOException  {
   }
}
class FlushableDoesNothing implements Flushable  {
   public void flush() throws IOException  {
   }
}

... qui peut être implémenté en tant que classe interne anonyme:

public WriterForAppendable(Appendable apbl)  {
   if(apbl == null)  {
      throw  new NullPointerException("apbl");
   }
   this.apbl = apbl;

   //Avoids instanceof at every call to flush() and close()
   flshbl = ((apbl instanceof Flushable)
      ?  (Flushable)apbl
      :  new Flushable()  {
            public void flush() throws IOException  {
            }
         });
   clsbl = ((apbl instanceof Closeable)
      ?  (Closeable)apbl
      :  new Closeable()  {
            public void close() throws IOException  {
            }
         });
}

//the rest of the class goes here...

}

Pour être plus efficaces, ces foncteurs ne rien faire doivent être implémentés en tant qu'objets finaux statiques. Et avec ça, voici la version finale de notre classe:

package  xbn.z.xmpl.lang.functor;
   import  Java.io.Closeable;
   import  Java.io.Flushable;
   import  Java.io.IOException;
   import  Java.io.Writer;
public class WriterForAppendable extends Writer  {
   private final Appendable apbl;
   private final Flushable  flshbl;
   private final Closeable  clsbl;

   //Do-nothing functors
      private static final Flushable FLUSHABLE_DO_NOTHING = new Flushable()  {
         public void flush() throws IOException  {
         }
      };
      private static final Closeable CLOSEABLE_DO_NOTHING = new Closeable()  {
         public void close() throws IOException  {
         }
      };

   public WriterForAppendable(Appendable apbl)  {
      if(apbl == null)  {
         throw  new NullPointerException("apbl");
      }
      this.apbl = apbl;

      //Avoids instanceof at every call to flush() and close()
      flshbl = ((apbl instanceof Flushable)
         ?  (Flushable)apbl
         :  FLUSHABLE_DO_NOTHING);
      clsbl = ((apbl instanceof Closeable)
         ?  (Closeable)apbl
         :  CLOSEABLE_DO_NOTHING);
   }

   public void write(char[] a_c, int i_ndexStart, int i_ndexEndX) throws IOException {
      apbl.append(String.valueOf(a_c), i_ndexStart, i_ndexEndX);
   }
   public Writer append(char c_c) throws IOException {
      apbl.append(c_c);
      return  this;
   }
   public Writer append(CharSequence c_q) throws IOException {
      apbl.append(c_q);
      return  this;
   }
   public Writer append(CharSequence c_q, int i_ndexStart, int i_ndexEndX) throws IOException  {
      apbl.append(c_q, i_ndexStart, i_ndexEndX);
      return  this;
   }
   public void flush() throws IOException {
      flshbl.flush();
   }
   public void close() throws IOException {
      flush();
      clsbl.close();
   }
}

Cet exemple particulier vient de cette question on stackoverflow . Une version entièrement fonctionnelle et entièrement documentée de cet exemple (y compris une fonction de test) se trouve au bas de ce message (au-dessus de la réponse).

Implémentation de foncteurs avec une énumération

Laissant notre Writer-Appendable exemple, jetons un œil à une autre façon d'implémenter les Functors: avec un Enum.

Par exemple, cette énumération a une fonction move pour chaque direction cardinale:

public enum CardinalDirection  {
   NORTH(new MoveNorth()),
   SOUTH(new MoveSouth()),
   EAST(new MoveEast()),
   WEST(new MoveWest());

   private final MoveInDirection dirFunc;

   CardinalDirection(MoveInDirection dirFunc)  {
      if(dirFunc == null)  {
         throw  new NullPointerException("dirFunc");
      }
      this.dirFunc = dirFunc;
   }
   public void move(int steps)  {
      dirFunc.move(steps);
   }
}

Son constructeur nécessite un objet MoveInDirection (qui est une interface, mais pourrait également être une classe abstraite):

interface MoveInDirection {
 void move (étapes int); 
}

Il existe naturellement quatre implémentations concrètes de cette interface, une par direction. Voici une implémentation triviale pour le nord:

la classe MoveNorth implémente MoveInDirection {[.....) public void move (int étapes) {
 System.out.println ("Moved" + étapes + "étapes vers le nord."); 
} 
}

L'utilisation de ce Functor se fait avec ce simple appel:

CardinalDirection.WEST.move (3);

Qui, dans notre exemple, renvoie ceci à la console:

Déplacé de 3 marches vers l'ouest.

Et voici un exemple de travail complet:

/**
   <P>Demonstrates a Functor implemented as an Enum.</P>

   <P>{@code Java EnumFunctorXmpl}</P>
 **/
public class EnumFunctorXmpl  {
   public static final void main(String[] ignored)  {
       CardinalDirection.WEST.move(3);
       CardinalDirection.NORTH.move(2);
       CardinalDirection.EAST.move(15);
   }
}
enum CardinalDirection  {
   NORTH(new MoveNorth()),
   SOUTH(new MoveSouth()),
   EAST(new MoveEast()),
   WEST(new MoveWest());
   private final MoveInDirection dirFunc;
   CardinalDirection(MoveInDirection dirFunc)  {
      if(dirFunc == null)  {
         throw  new NullPointerException("dirFunc");
      }
      this.dirFunc = dirFunc;
   }
   public void move(int steps)  {
      dirFunc.move(steps);
   }
}
interface MoveInDirection  {
   void move(int steps);
}
class MoveNorth implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps north.");
   }
}
class MoveSouth implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps south.");
   }
}
class MoveEast implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps east.");
   }
}
class MoveWest implements MoveInDirection  {
   public void move(int steps)  {
      System.out.println("Moved " + steps + " steps west.");
   }
}

Production:

[C:\Java_code] Java EnumFunctorXmpl 
 Déplacement de 3 marches vers l'ouest. 
 Déplacement de 2 marches vers le nord.

Je n'ai pas encore commencé avec Java 8 encore, donc je ne peux pas encore écrire la section Lambdas :)

6
aliteralmind

Prendre le concept d'application de fonction

f.apply(x)

Inverse

x.map(f)

Appelez x a functor

interface Functor<T> {
    Functor<R> map(Function<T, R> f);
}
1
user2418306