Envoi dynamic dans Haskell

Les programmes écrits par exemple dans Java reposent beaucoup sur la répartition dynamic. Comment ces programmes s’expriment-ils dans des langages fonctionnels tels que Haskell? En d’autres termes, quelle est la manière de Haskell d’exprimer l’idée sous “dispatch dynamic”?

La réponse est d’une simplicité trompeuse: les fonctions d’ordre supérieur. Un object avec des méthodes virtuelles dans un langage OO n’est rien d’autre qu’un enregistrement glorifié de fonctions avec un état local. Dans Haskell, vous pouvez utiliser des enregistrements de fonctions directement et stocker l’état local dans leurs fermetures.

Plus concrètement, un object OO consiste en:

  • Un pointeur (vptr) vers une vtable (table de méthode virtuelle), qui contient des implémentations pour les méthodes virtuelles de la classe de l’object. En d’autres termes, un tas de pointeurs de fonctions; un enregistrement des fonctions. Notamment chaque fonction a un paramètre caché qui est l’object lui-même, qui est passé implicitement.
  • Les membres de données de l’object (l’état local)

La plupart du temps, tout l’édifice des objects et des fonctions virtuelles semble être une solution de contournement élaborée pour le manque de prise en charge des fermetures.

Par exemple, considérez l’interface de Comparator de Java:

 public interface Comparator { int compare(T o1, T o2); // virtual (per default) } 

Et supposons que vous vouliez l’utiliser pour sortinger une liste de chaînes en fonction des N ° caractères des chaînes (supposez qu’elles soient assez longues). Vous définiriez une classe:

 public class MyComparator implements Comparator { private final int _n; MyComparator(int n) { _n = n; } int compare(Ssortingng s1, Ssortingng s2) { return s1.charAt(_n) - s2.charAt(_n); } } 

Et puis vous l’utilisez:

 Collections.sort(myList, new MyComparator(5)); 

En Haskell, vous le feriez comme ceci:

 sortBy :: (a -> a -> Ordering) -> [a] -> [a] myComparator :: Int -> (Ssortingng -> Ssortingng -> Ordering) myComparator n = \s1 s2 -> (s1 !! n) `compare` (s2 !! n) -- n is implicitly stored in the closure of the function we return foo = sortBy (myComparator 5) myList 

Si vous n’êtes pas familier avec Haskell, voici comment cela ressemblerait à une sorte de pseudo-Java: (je ne le ferai qu’une fois)

 public void  sortBy(List list, Ordering FUNCTION(T, T) comparator) { ... } public (Ordering FUNCTION(Ssortingng, Ssortingng)) myComparator(int n) { return FUNCTION(Ssortingng s1, Ssortingng s2) { return s1[n].compare(s2[n]); } } public void foo() { sortBy(myList, myComparator(5)); } 

Notez que nous n’avons défini aucun type. Tout ce que nous avons utilisé sont des fonctions. Dans les deux cas, la “charge utile” que nous avons passée à la fonction de sorting était une fonction qui prend deux éléments et donne leur ordre relatif. Dans un cas, cela a été réalisé en définissant un type implémentant une interface, en implémentant sa fonction virtuelle de la manière appropriée et en transmettant un object de ce type. dans l’autre cas, nous venons de passer directement une fonction. Dans les deux cas, nous avons stocké un entier interne dans la chose que nous avons passée à la fonction de sorting. Dans un cas, cela a été fait en ajoutant un membre de données privé à notre type, dans l’autre en faisant simplement référence à celui-ci dans notre fonction, ce qui a entraîné sa conservation dans la fermeture de la fonction.

Prenons l’exemple plus élaboré d’un widget avec des gestionnaires d’événements:

 public class Widget { public void onMouseClick(int x, int y) { } public void onKeyPress(Key key) { } public void paint() { } ... } public class MyWidget extends Widget { private Foo _foo; private Bar _bar; MyWidget(...) { _foo = something; _bar = something; } public void onMouseClick(int x, int y) { ...do stuff with _foo and _bar... } } 

En Haskell, vous pouvez le faire comme ceci:

 data Widget = Widget { onMouseClick :: Int -> Int -> IO (), onKeyPress :: Key -> IO (), paint :: IO (), ... } constructMyWidget :: ... -> IO Widget constructMyWidget = do foo <- newIORef someFoo bar <- newIORef someBar return $ Widget { onMouseClick = \xy -> do ... do stuff with foo and bar ..., onKeyPress = \key -> do ..., paint = do ... } 

Notez à nouveau qu’après le Widget initial, nous n’avons défini aucun type. Nous avons seulement écrit une fonction pour construire un enregistrement de fonctions et stocker des éléments dans leurs fermetures. Ce qui, la plupart du temps, est également la seule raison de définir une sous-classe dans un langage OO. La seule différence avec notre exemple précédent est qu’au lieu d’une fonction, il y a plusieurs fonctions, qui dans le cas de Java sont encodées simplement en mettant plus d’une fonction dans l’interface (et ses implémentations), et dans Haskell en passant une seule fonction. (Nous aurions pu passer un enregistrement contenant une seule fonction dans l’exemple précédent, mais nous n’en avions pas envie.)

(Il est intéressant de noter que la plupart du temps, vous n’avez pas besoin de dispatch dynamic. Si vous voulez juste sortinger une liste en fonction du classement par défaut du type, vous utiliserez simplement sort :: Ord a => [a] -> [a] , qui utilise l’instance Ord définie pour le type donné, qui est sélectionné de manière statique.)

Envoi dynamic basé sur type

Une différence entre l’approche Java et l’approche Haskell ci-dessus est qu’avec l’approche Java, le comportement d’un object (à l’exception de son état local) est déterminé par son type (ou moins, chaque implémentation nécessite un nouveau type). Dans Haskell, nous enregistrons nos fonctions comme bon nous semble. La plupart du temps, il s’agit d’une victoire pure (flexibilité acquise, rien de perdu), mais supposons que pour une raison quelconque, nous la souhaitons à la manière de Java. Dans ce cas, la manière de procéder, comme mentionné par d’autres réponses, est les classes de type et les existentiels.

Pour continuer notre exemple Widget , supposons que nous souhaitons que l’implémentation d’un Widget suive son type (pour exiger un nouveau type pour chaque implémentation). Nous définissons une classe de type pour mapper le type à son implémentation:

 -- the same record as before, we just gave it a different name data WidgetImpl = WidgetImpl { onMouseClick :: Int -> Int -> IO (), onKeyPress :: Key -> IO (), paint :: IO (), ... } class IsWidget a where widgetImpl :: a -> WidgetImpl data Widget = forall a. IsWidget a => Widget a sendClick :: Int -> Int -> Widget -> IO () sendClick xy (Widget a) = onMouseClick (widgetImpl a) xy data MyWidget = MyWidget { foo :: IORef Foo, bar :: IORef Bar } constructMyWidget :: ... -> IO MyWidget constructMyWidget = do foo_ <- newIORef someFoo bar_ <- newIORef someBar return $ MyWidget { foo = foo_, bar = bar_ } instance IsWidget MyWidget where widgetImpl myWidget = WidgetImpl { onMouseClick = \xy -> do ... do stuff with (foo myWidget) and (bar myWidget) ..., onKeyPress = \key -> do ..., paint = do ... } 

C’est un peu gênant que nous n’ayons une classe que pour obtenir un enregistrement des fonctions, que nous devons ensuite utiliser séparément. Je l’ai seulement fait de cette façon pour illustrer des aspects distincts des classes de type: elles sont juste des enregistrements de fonctions glorifiés (que nous utilisons ci-dessous) avec un peu de magie où le compilateur insère l’enregistrement approprié basé sur le type inféré , et continuez à utiliser ci-dessous). Simplifions:

 class IsWidget a where onMouseClick :: Int -> Int -> a -> IO () onKeyPress :: Key -> a -> IO () paint :: a -> IO () ... instance IsWidget MyWidget where onMouseClick xy myWidget = ... do stuff with (foo myWidget) and (bar myWidget) ... onKeyPress key myWidget = ... paint myWidget = ... sendClick :: Int -> Int -> Widget -> IO () sendClick xy (Widget a) = onMouseClick xya -- the rest is unchanged from above 

Ce style est souvent adopté par les personnes issues de langages OO, car il est plus familier et plus proche d’un mappage one-to-one que le font les langages OO. Mais dans la plupart des cas, c’est simplement plus élaboré et moins flexible que l’approche décrite dans la première section. La raison en est que si la seule chose importante à propos des différents widgets est la manière dont ils implémentent les fonctions du widget, il est inutile de créer des types, des instances de l’interface pour ces types, puis d’en extraire à nouveau le type sous-jacent. un wrapper existentiel: il est plus simple de faire passer les fonctions directement.

Un avantage auquel je peux penser est que, même si Haskell n’a pas de sous-typage, il possède un “sous-classement” (probablement mieux connu sous le nom de sous-interface ou de sous-contrainte). Par exemple, vous pouvez faire:

 class IsWidget a => IsWidgetExtra a where ...additional methods to implement... 

et puis, avec n’importe quel type pour lequel vous avez IsWidgetExtra , vous pouvez également utiliser les méthodes de IsWidget de IsWidget transparente. La seule alternative à l’approche basée sur les enregistrements est d’avoir des enregistrements dans les enregistrements, ce qui implique un wrapping et un déroulage manuel des enregistrements internes. Mais cela ne serait avantageux que si vous vouliez émuler explicitement la hiérarchie de classes profonde d’un langage OO, ce que vous ne feriez que si vous souhaitiez vous rendre la vie difficile. (Notez également que Haskell n’a aucun moyen IsWidget de IsWidgetExtra dynamicment IsWidget vers IsWidgetExtra . Mais il y a ifcxt )

(Qu’en est-il des avantages de l’approche basée sur les enregistrements? En plus de ne pas avoir à définir un nouveau type chaque fois que vous voulez faire une nouvelle chose, les enregistrements sont simples, et les valeurs sont beaucoup plus faciles à manipuler. , par exemple, écrire une fonction qui prend un Widget comme argument et en créer un nouveau, avec certaines choses différentes et d’autres conservées, ce qui est un peu comme la sous-classification d’un paramètre de modèle en C ++. )

Glossaire

  • Fonction d’ordre supérieur: une fonction qui prend d’autres fonctions comme arguments (ou les retourne en tant que résultats)

  • Record: struct (classe avec membres de données publiques et rien d’autre). Aussi appelé dictionnaire.

  • Fermeture: Les langages fonctionnels (et bien d’autres) vous permettent de définir des fonctions locales (fonctions au sein de fonctions, lambdas) faisant référence à des éléments du site de définition (par exemple, les arguments de la fonction externe) que vous attendez normalement. à garder autour, mais sont dans la “fermeture” de la fonction. Alternativement, si vous avez une fonction comme plus qui prend deux pouces et retourne un int, vous pouvez l’appliquer à un seul argument, par exemple 5 , et le résultat sera une fonction qui prend un int et retourne un int, en ajoutant 5 à le – dans ce cas le 5 est également stocké dans la fermeture de la fonction résultante. (Dans d’autres contextes, la “fermeture” est aussi parfois utilisée pour signifier “une fonction avec une fermeture”.)

  • Classe de type: pas la même que celle d’une classe dans un langage OO. Un peu comme une interface, mais aussi très différente. Voir ici

EDIT 29-11-14: Bien que je pense que le kernel de cette réponse est toujours essentiellement correct (les HOF dans Haskell correspondent aux méthodes virtuelles dans la POO), mes jugements de valeur ont été quelque peu nuancés depuis que je l’ai écrit. En particulier, je pense maintenant que ni l’approche de Haskell ni celle de l’OOP n’est ssortingctement «plus fondamentale» que l’autre. Voir ce commentaire reddit .

Il est surprenant de constater combien de fois vous n’avez pas vraiment besoin d’ une répartition dynamic , simplement d’un polymorphism.

Par exemple, si vous écrivez une fonction qui sortinge toutes les données d’une liste, vous voulez qu’elle soit polymorphe. (C’est-à-dire que vous ne voulez pas avoir à réimplémenter cette fonction pour chaque type, à la main. Ce serait mauvais.) Mais vous n’avez besoin de rien de dynamic ; vous savez à la compilation ce qui est réellement dans la liste ou les listes que vous voulez sortinger. Donc, dans ce cas, vous n’avez pas du tout besoin d’une recherche de type à l’ exécution .

Dans Haskell, si vous voulez simplement déplacer des choses et que vous n’avez pas besoin de savoir quel type il est, vous pouvez utiliser ce qu’on appelle le “polymorphism paramésortingque”, qui ressemble à peu près aux génériques Java ou aux modèles C ++. Si vous devez être capable d’appliquer une fonction aux données (par exemple, pour sortinger les données dont vous avez besoin pour les comparaisons de commandes), vous pouvez transmettre la fonction en argument. Alternativement, Haskell a une chose un peu comme une interface Java, et vous pouvez dire “cette fonction de sorting accepte tout type de données qui implémente cette interface”.

Jusqu’à présent, pas de dissortingbution dynamic du tout, seulement statique. Notez également que, comme vous pouvez passer des fonctions en tant qu’arguments, vous pouvez effectuer une “répartition” manuelle à la main.

Si vous avez vraiment besoin d’ une répartition dynamic réelle, vous pouvez utiliser des «types existentiels» ou utiliser la bibliothèque Data.Dynamic et des astuces similaires.

Le polymorphism ad hoc se fait via des classes de caractères . Plus de DD de type OOP sont émulés avec des types existentiels .

Peut-être avez-vous besoin d’un ADT plus de correspondance de modèle?

 data Animal = Dog {dogName :: Ssortingng} | Cat {catName :: Ssortingng} | Unicorn say :: Animal -> Ssortingng say (Dog {dogName = name}) = "Woof Woof, my name is " ++ name say (Cat {catName = name}) = "Meow meow, my name is " ++ name say Unicorn = "Unicorns do not talk"