59 votes

Maintien d'un état complexe à Haskell

Supposons que vous êtes en train de construire une assez grande simulation en Haskell. Il ya beaucoup de différents types d'entités, dont les attributs de mise à jour de la simulation progresse. Disons, par exemple, que vos entités sont appelées des Singes, des Éléphants, des Ours, etc..

Quelle est votre méthode préférée pour le maintien de ces entités des etats-unis?

La première et la plus évidente de l'approche que j'ai pensé a ceci:

mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String
mainLoop monkeys elephants bears =
  let monkeys'   = updateMonkeys   monkeys
      elephants' = updateElephants elephants
      bears'     = updateBears     bears
  in
    if shouldExit monkeys elephants bears then "Done" else
      mainLoop monkeys' elephants' bears'

C'est déjà moche d'avoir chaque type d'entité est explicitement mentionné dans l' mainLoop signature de fonction. Vous pouvez imaginer comment il se absolument horrible si vous avez, disons, 20 types d'entités. (20 n'est pas déraisonnable pour des simulations complexes.) Donc, je pense que c'est une démarche inacceptable. Mais sa grâce qui sauve, c'est que des fonctions comme updateMonkeys sont très explicites en ce qu'ils font: Ils ont une liste de Singes et de retourner un nouveau.

Alors la prochaine pensée serait de rouler le tout dans une grande structure de données qui contient de l'etat, donc le nettoyage de la signature de l' mainLoop:

mainLoop :: GameState -> String
mainLoop gs0 =
  let gs1 = updateMonkeys   gs0
      gs2 = updateElephants gs1
      gs3 = updateBears     gs2
  in
    if shouldExit gs0 then "Done" else
      mainLoop gs3

Certains suggèrent de nous envelopper GameState dans un État de Monade et appelez - updateMonkeys etc. en do. C'est très bien. Certains suggèrent plutôt nous les nettoyer avec la fonction de composition. Aussi bien, je pense. (BTW, je suis un novice avec Haskell, donc peut-être que je me trompe sur certains de cela.)

Mais le problème est, les fonctions comme updateMonkeys ne pas vous donner de l'information utile à partir de leur type de signature. Vous ne pouvez pas vraiment être sûr de ce qu'ils font. Bien sûr, updateMonkeys est un nom descriptif, mais c'est une maigre consolation. Lorsque je passe un dieu de l'objet et de dire "s'il vous plaît mettre à jour mon état global," je me sens comme nous sommes de retour à l'impératif monde. Il se sent comme les variables globales par un autre nom: Vous avez une fonction qui fait quelque chose à l'état, vous l'appelez, et vous de l'espoir pour le meilleur. (Je suppose que vous avez toujours éviter certains problèmes de simultanéité qui serait présent avec des variables globales dans un programme impératif. Mais meh, la concurrence n'est pas loin d'être la seule chose de mal avec les variables globales.)

Un autre problème est le suivant: Supposons que les objets doivent interagir. Par exemple, nous avons une fonction comme ceci:

stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
  (elongateEvilGrin elephant, decrementHealth monkey)

Dire ce qui est appelé en updateElephants, parce que c'est là que nous vérifions pour voir si les éléphants sont en piétinant gamme de singes. Comment avez-vous élégamment propager les modifications à la fois les singes et les éléphants dans ce scénario? Dans notre deuxième exemple, updateElephants prend et retourne un dieu de l'objet, de sorte qu'il pourrait en effet à la fois des changements. Mais cela brouille les eaux plus loin et renforce mon point de vue: Avec le dieu objet, vous êtes effectivement juste la mutation des variables globales. Et si vous n'utilisez pas le dieu objet, je ne suis pas sûr de savoir comment vous feriez propager ces types de changements.

Que faire? Certes, beaucoup de programmes ont besoin pour gérer la complexité de la situation, donc je suppose que il y a certains bien connus des approches à ce problème.

Juste pour le plaisir de comparaison, voici comment je pourrais résoudre le problème dans le monde de la programmation orientée objet. Il n'y aurait Monkey, Elephant, etc. objets. J'aurais probablement les méthodes de la classe à faire des recherches dans l'ensemble de tous les animaux vivants. Peut-être que vous pourriez rechercher par emplacement, par ID, que ce soit. Merci pour les structures de données sous-jacentes les fonctions de recherche, ils avaient séjour allouées sur le tas. (Je suis en supposant GC ou de comptage de référence.) Leur membre variables se muté tous les temps. N'importe quelle méthode de la classe serait capable de muter un animal vivant de toute autre classe. E. g. un Elephant pourrait avoir un stomp méthode de décrémenter la santé d'un passé- Monkey objet, et il n'y aurait pas besoin de passer que

De même, dans un Erlang ou autres acteur-conception orientée, vous pouvez résoudre ces problèmes assez élégamment: Chaque acteur conserve sa propre boucle, et donc sa propre état, de sorte que vous n'avez jamais besoin d'un dieu de l'objet. Et de la transmission de message permet à un objet d'activités à déclencher des changements dans d'autres objets, sans passer par un tas de choses tout le chemin du retour, la pile d'appel. Pourtant, j'ai entendu dire que les acteurs de Haskell sont désapprouvées.

Enfin, toutes mes excuses pour ce mur de texte. Mon espoir est que la lenteur de cette question se justifie par son utilité à mes collègues Haskell débutants.

30voto

ertes Points 3012

La réponse est fonctionnel réactif de programmation (PRF). Il il un hybride de deux styles de codage: le composant de gestion de l'état et temps-dépendante des valeurs. Depuis le FRP est en fait toute une famille de modèles de conception, je veux être plus précis: je recommande Netwire.

L'idée sous-jacente est très simple: Vous écrivez beaucoup de petits, des composantes autonomes, chacune avec leur propre état. C'est pratiquement équivalent à temps dépendant de valeurs, parce que chaque fois que vous interrogez un tel composant, vous obtiendrez une réponse différente et provoquer un état local de mise à jour. Ensuite à vous de combiner ces éléments pour constituer votre programme actuel.

Bien que cela semble compliqué et inefficace, il est en fait qu'une très fine couche autour des fonctions classiques. Le modèle de conception mis en œuvre par Netwire est inspiré par AFRP (Arrowized Fonctionnel Réactif de Programmation). C'est sans doute assez différent pour mériter son nom propre (WFRP?). Vous pouvez lire le tutoriel.

En tout cas une petite démo de la façon suivante. Vos blocs de construction sont des fils:

myWire :: WireP A B

Pensez à cela comme un composant. Il est variable dans le temps la valeur de type B qui dépend d'une variable dans le temps la valeur de type Un, par exemple une particule dans un simulateur:

particle :: WireP [Particle] Particle

Il dépend d'une liste de particules (par exemple toutes les particules existantes) et est elle-même une particule. Nous allons utiliser un modèle prédéfini fil (simplifié):

time :: WireP a Time

C'est variable dans le temps la valeur de type de Temps (= Double). Eh bien, c'est le temps lui-même (en commençant à 0, comptés à partir à chaque fois que le câble de réseau a été lancé). Car il ne dépend pas d'une autre variable dans le temps la valeur que vous pouvez nourrir tout ce que vous voulez, d'où le polymorphe type d'entrée. Il y a également une constante fils (variant dans le temps des valeurs qui ne changent pas au fil du temps):

pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer

Pour connecter deux câbles que vous utilisez simplement catégorique de la composition:

integral_ 3 . 15

Cela vous donne une horloge à 15x temps réel de la vitesse (l'intégrale du 15 au fil du temps) à partir de 3 (l'intégration constante). Grâce aux diverses instances de classe fils sont très pratique pour mélanger. Vous pouvez l'utilisez aussi bien les opérateurs que applicative style ou le style de flèche. Voulez une horloge qui commence à 10 et est deux fois le temps réel de la vitesse?

10 + 2*time

Voulez une particule qui commence et (0, 0) avec (0, 0) de vitesse et accélère avec (2, 1) par seconde par seconde?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)

Souhaitez afficher les statistiques lorsque l'utilisateur appuie sur la barre d'espace?

stats . keyDown Spacebar <|> "stats currently disabled"

C'est juste une petite fraction de ce Netwire peut faire pour vous.

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