34 votes

Polymorphisme de sous-type dans Haskell

La construction d'une hiérarchie de GUI widget classes est assez bien d'un exercice standard de programmation orientée objet. Vous avez une sorte de résumé Widget classe, avec un résumé, sous-classe de widgets qui peuvent contenir d'autres widgets, et puis vous avez une profusion de nouvelles classes abstraites pour les widgets que support textuel de l'écran, des widgets qui soutien le focus d'entrée, les widgets qui ont une valeur booléenne de l'état, jusqu'à de véritables classes de béton tels que des boutons, des curseurs, des barres de défilement, des cases à cocher, etc.

Ma question est: Quelle est la meilleure façon de le faire en Haskell?

Il y a un certain nombre de choses qui rendent la construction d'un Haskell GUI difficiles, mais sont pas partie de ma question. Faire interactives I/O est légèrement délicate en Haskell. La mise en œuvre d'un GUI signifie presque toujours à écrire un wrapper à un très bas niveau en C ou C++ de la bibliothèque. Et les gens qui écrivent ces wrappers tendance à copier les API existantes verbatim (probablement pour quelqu'un qui sait le enveloppés bibliothèque sentirez à la maison). Ces problèmes ne m'intéresse pas pour le moment. Je suis intéressé purement à la meilleure façon de modéliser le sous-type de polymorphisme en Haskell.

Ce genre de propriétés que nous voulons de notre hypothétique de la bibliothèque d'interface graphique? Eh bien, nous voulons qu'il soit possible d'ajouter de nouveaux types de widgets à tout moment. (En d'autres termes, un ensemble fermé de possible widgets n'est pas bon.) Nous voulons minimiser la duplication de code. (Il y a beaucoup de types de widgets!) Idéalement, nous voulons être en mesure de préciser un type de widget, si nécessaire, mais aussi pour être en mesure de gérer les collections de tout type de widget, si nécessaire.

Le tout est bien sûr trivial dans un self-respect de langage OO. Mais quelle est la meilleure façon de le faire en Haskell? Je peux penser à plusieurs approches, mais je ne suis pas sûr qu'on serait "meilleur".

34voto

dflemstr Points 18999

Réelle widget objets est quelque chose qui est très orientée objet. Une technique couramment utilisée dans le monde fonctionnelle est d'utiliser à la place Fonctionnel Réactif de Programmation (PRF). Je vais décrire brièvement ce que d'une bibliothèque de widgets dans le plus pur Haskell ressemblerait lors de l'utilisation de PRF.


tl/dr: Vous n'avez pas à traiter "Widget objets", vous permet de gérer des collections de "flux d'événements" à la place, et ne se soucient pas de ce qui les widgets ou lorsque ces flux proviennent.


En fibre de verre, il y a la notion de base d'un Event a, qui peut être vu comme une liste infinie [(Time, a)]. Donc, si vous voulez le modèle d'un compteur qui compte, on l'écrit en tant que [(00:01, 1), (00:02, 4), (00.03, 7), ...], qui associe un compteur spécifique de la valeur à un moment donné. Si vous souhaitez modéliser un bouton est enfoncé, vous produisez un [(00:01, ButtonPressed), (00:02, ButtonReleased), ...]

Il y a aussi communément ce qu'on appelle un Signal a, qui est comme un Event a, sauf que la valeur modélisée est continue. Vous n'avez pas un ensemble discret de valeurs à des moments précis, mais vous pouvez demander à l' Signal de sa valeur, disons, 00:02:231 et il vous donnera la valeur 4.754 ou quelque chose. Pensez à un signal comme un signal analogique comme sur un cœur jauge de charge (électrocardiographiques de l'appareil/le moniteur de Holter) à l'hôpital: c'est une ligne continue qui saute haut et en bas, mais ne fait jamais un "écart". Une fenêtre ne toujours avoir un titre, par exemple (mais c'est peut-être la chaîne vide), de sorte que vous pouvez toujours demander à sa valeur.


Dans une bibliothèque d'interface graphique, sur un niveau bas, il y avait un mouseMovement :: Event (Int, Int) et mouseAction :: Event (MouseButton, MouseAction) ou quelque chose. L' mouseMovement est le réel USB/souris PS2 sortie, de sorte que vous obtenez seulement des différences de positions comme des événements (par exemple, lorsque l'utilisateur déplace la souris vers le haut, vous obtiendrez cas (12:35:235, (0, -5)). Vous seriez alors en mesure de "s'intégrer" ou plutôt à "accumuler" le mouvement des événements pour obtenir un mousePosition :: Signal (Int, Int) qui vous a donné absolue les coordonnées de la souris. mousePosition pourrait également prendre en considération absolue des périphériques de pointage, tels que des écrans tactiles, ou d'événements du système d'exploitation qui repositionner le curseur de la souris, etc.

De même pour un clavier, il y avait un keyboardAction :: Event (Key, Action), et l'on pourrait aussi "intégrer" que les flux d'événements en keyboardState :: Signal (Key -> KeyState) qui vous permet de lire une clé de l'état à n'importe quel point dans le temps.


Les choses se compliquent quand vous voulez dessiner des trucs sur l'écran et interagir avec les widgets.

Pour créer un guichet unique, on aurait une "fonction magique" appelé:

window :: Event DrawCommand -> Signal WindowIcon -> Signal WindowTitle -> ...
       -> FRP (Event (Int, Int) {- mouse events -},
               Event (Key, Action) {- key events -},
               ...)

La fonction est magique parce qu'il faudrait appeler le système d'exploitation des fonctions spécifiques et de créer une fenêtre (à moins que l'OS lui-même est le fibre de verre, mais je doute que). C'est aussi pourquoi il est dans l' FRP monade, car il s'agirait createWindow et setTitle et registerKeyCallback etc en IO monade derrière les coulisses.

On pourrait bien sûr le groupe de l'ensemble de ces valeurs dans des structures de données de sorte qu'il y aurait:

window :: WindowProperties -> ReactiveWidget
       -> FRP (ReactiveWindow, ReactiveWidget)

L' WindowProperties sont des signaux et des événements qui déterminent l'aspect et le comportement de la fenêtre (par exemple, si il devrait y avoir les boutons fermer, ce que le titre devrait être, etc.).

L' ReactiveWidget représente S et Es qui sont des évènements souris et clavier, dans le cas où vous souhaitez émuler les clics de souris à partir de votre application, et un Event DrawCommand que représente le flux des choses que vous voulez dessiner sur la fenêtre. Cette structure de données est commune à tous les widgets.

L' ReactiveWindow représente des événements comme la fenêtre réduite, etc, et la sortie ReactiveWidget représente la souris et le clavier des événements provenant de l'extérieur/à l'utilisateur.

Puis on va créer une réelle widget, disons un bouton-poussoir. Il aurait la signature:

button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)

L' ButtonProperties aurait à déterminer la couleur/texte/etc sur le bouton, et l' ReactiveButton contiendrait par exemple, un Event ButtonAction et Signal ButtonState lire l'état du bouton.

Notez que l' button de la fonction est une fonction pure, puisqu'elle ne dépend que de la pure PRF valeurs, comme les événements et les signaux.

Si l'on veut groupe de widgets (par exemple, les empiler horizontalement), on aurait pu créer par exemple un:

horizontalLayout :: HLayoutProperties -> ReactiveWidget
                 -> (ReactiveLayout, ReactiveWidget)

L' HLayoutProperties devrait contenir des informations sur la frontière de tailles et de l' ReactiveWidgets pour le contenu des widgets. L' ReactiveLayout serait alors contenir un [ReactiveWidget] avec un élément pour chaque enfant widget.

Ce que la disposition n'est qu'il aurait une interne Signal [Int] qui a déterminé la hauteur de chaque widget dans la mise en page. Il serait alors bénéficier de tous les événements à partir de l'entrée ReactiveWidget, alors basée sur la structure de la partition, sélectionnez une sortie ReactiveWidget pour envoyer l'événement à, aussi en attendant la transformation de l'origine, par exemple les événements de la souris par le décalage de la partition.


Pour démontrer comment cette API permettrait de travail, estime que ce programme est:

main = runFRP $ do rec -- Recursive do, lets us use winInp lazily before it is defined

  -- Create window:
  (win, winOut) <- window winProps winInp

      -- Create some arbitrary layout with our 2 widgets:
  let (lay, layOut) = layout (def { widgets = [butOut, labOut] }) layInp
      -- Create a button:
      (but, butOut) = button butProps butInp
      -- Create a label:
      (lab, labOut) = label labProps labInp
      -- Connect the layout input to the window output
      layInp = winOut
      -- Connect the layout output to the window input
      winInp = layOut
      -- Get the spliced input from the layout
      [butInp, layInp] = layoutWidgets lay
      -- "pure" is of course from Applicative Functors and indicates a constant Signal
      winProps = def { title = pure "Hello, World!", size = pure (800, 600) }
      butProps = def { title = pure "Click me!" }
      labProps = def { text = reactiveIf
                              (buttonPressed but)
                              (pure "Button pressed") (pure "Button not pressed") }
  return ()

(def est de Data.Default en data-default)

Cela crée un événement graphique, comme suit:

     Input events ->            Input events ->
win ---------------------- lay ---------------------- but \
     <- Draw commands etc.  \   <- Draw commands etc.      | | Button press ev.
                             \  Input events ->            | V
                              \---------------------- lab /
                                <- Draw commands etc.

Notez qu'il ne doit pas y avoir de "widget objets" n'importe où. Une mise en page est tout simplement une fonction qui transforme l'entrée et la sortie des événements selon un système de partitionnement, vous pouvez utiliser le flux d'événements, vous avez accès à des widgets, ou vous pouvez laisser un autre sous-système de générer le flux entièrement. Il en va de même pour les boutons et les étiquettes: ils sont simplement des fonctions de conversion sur les événements en tirer des commandes, ou des choses semblables. C'est une représentation de découplage total, et très flexible dans la nature.

10voto

Heinrich Apfelmus Points 7200

Le wxHaskell la bibliothèque d'interface graphique fait un excellent usage de fantôme types de modèle d'un widget de la hiérarchie.

L'idée est la suivante: tous les widgets de partager la même mise en œuvre, à savoir qu'ils sont des étrangers, des pointeurs vers des objets en C++. Toutefois, cela ne signifie pas que tous les widgets doivent avoir le même type. Au lieu de cela, nous pouvons construire une hiérarchie comme ceci:

type Object a = ForeignPtr a

data CWindow a
data CControl a
data CButton a

type Window  a = Object  (CWindow a)
type Control a = Window  (CControl a)
type Button  a = Control (CButton a)

De cette façon, une valeur de type Control A correspond également au type Window b, de sorte que vous pouvez utiliser des contrôles que windows, mais pas l'inverse. Comme vous pouvez le voir, le sous-typage est mis en œuvre par un ensemble de paramètres de type.

Pour en savoir plus sur cette technique, voir la section 5 Dan Leijen du papier sur wxHaskell.


Notez que cette technique semble être limitée au cas où la représentation réelle de widgets est uniforme, c'est à dire toujours la même chose. Toutefois, je suis persuadé qu'avec de la réflexion, il peut être étendu au cas où les widgets ont des représentations différentes.

En particulier, l'observation, c'est que l'orientation de l'objet peut être modélisé par y compris les méthodes dans le type de données, comme ceci

data CWindow a = CWindow
    { close   :: IO ()
    , ...
    }
data CButton a = CButton
    { onClick :: (Mouse -> IO ()) -> IO ()
    , ...
    }

Le sous-typage peut sauver certains passe-partout ici, mais il n'est pas nécessaire.

7voto

Chris Kuklewicz Points 6789

Pour comprendre ce que la programmation orientée objet, comme le sous-type de polymorphisme, peut être fait en Haskell, vous pouvez regarder OOHaskell. Cela reproduit la sémantique d'une variété de puissants OOP type de systèmes, en gardant la plupart de l'inférence de type. Les données réelles de l'encodage n'est pas optimisé, mais je soupçonne le type de familles pourraient permettre de meilleures présentations.

La modélisation de l'interface de la hiérarchie (par exemple: Widget) peut être fait avec les classes de type. L'ajout de nouveaux cas est possible, et donc la série de widgets est ouvert. Si vous souhaitez la liste des widgets, puis GADTs peut être un succincte solution.

L'opération spéciale avec des sous-classes est upcasting et de passer.

C'est tout d'abord nécessaire d'avoir une collection de Widgets, et le résultat habituel est d'utiliser existentielle types. Il y a d'autres solutions intéressantes si vous lisez tous les bits de la HList de la bibliothèque. L'upcasting est assez facile et le compilateur peut être certain que toutes les distributions sont valides au moment de la compilation. Le passer est intrinsèquement dynamique et nécessite un certain type à l'exécution soutien à l'information, généralement de Données.Typable. Étant donné quelque chose comme Typable le passer est juste un autre type de classe, avec le résultat enveloppé dans Peut-être pour indiquer l'échec.

Il est réutilisable associé avec la plupart de cette, mais QusiQuoting et de modèles peuvent réduire cette. L'inférence de type peut encore largement de travail.

Je n'ai pas exploré la nouvelle Contrainte de genres et types, mais ils font augmenter les existentielle solution à upcasting et de passer.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X