Evolution de code en Haskell (partie 1) : conceptualisation

/img/post/Haskell_coding.jpg

Un logiciel n’est jamais terminé. Il ne fini pas d’évoluer pour satisfaire aussi bien de nouveaux besoins que de nouveaux enjeux technologiques. Un logiciel qui ne change pas, qui ne vit pas un refactoring continue, est un logiciel qui se meurt jusqu’à disparaître du marché parce que dépassé.

Nous savons faire évoluer une application écrite selon la POO en jouant sur les propriétés de rétention, de composition, et d’extension des objets, ces entités logicielles qui représentent les concepts adressés par le programme. Mais qu’en est-il en programmation fonctionnel ? Comment peuvent être représentés les concepts ? Comment un code, écrit avec un langage fonctionnel, peut-il évoluer face aux changements ? Je vous propose de montrer ces aspects par un petit tour d’horizon d’un programme écrit en Haskell.

Le programme sur lequel nous allons porter notre attention est un calculateur du résultat d’une bataille pour le jeu King’s Empire. Ce dernier est un jeu MMORPG (Massively Multiplayer Online Role Playing Game) pour smartphone et tablette disponible aussi bien sur iOS que sur Androïd.

Lorsque nous devons développer un logiciel, notre première approche est d’identifier les concepts sous-jacents au domaine adressé par l’application ; ici un monde fantastico-médiéval. Ces concepts peuvent être concrets (un paladin, un arsenal, …) comme abstraits (une bataille, une alliance, …). Selon l’approche de programmation que nous allons adopter, par le biais du langage, ces concepts seront représentés dans le programme sous une forme différente ; en POO, les concepts prendront la forme d’objets tandis qu’en programmation fonctionnelle, ils seront représentés par des fonctions. Chacune de ces formes permet l’abstraction, la composition et l’extensibilité des concepts implémentés, propriétés nécessaires à l’évolution du logiciel face non seulement aux demandes fonctionnelles mais aussi aux mutations technologiques. Dans ce billet, nous allons porter notre attention sur la conceptualisation ; l’évolution du code sera le sujet d’un billet ultérieur.

Le paradigme de programmation adopté ici est celui fonctionnel. Notre paladin pourrait être représentée par une fonction qui renverrait ces propriétés en attaque et en défense (vis à vis de l’infanterie, de la cavalerie, de la magie et de l’artillerie) :

import Data.Map as M (fromList, map, (!))

paladin = M.fromList [("attack", 60), ("infDef", 45), ("cavDef", 50), ("magDef", 90), ("artDef", 40)]

Évidemment, dans le jeu, nous utilisons surtout des paladins, des écuyers, etc. Le concept manipulé n’est donc pas en fait le paladin, mais les paladins (le symbole $ permet de remplacer les parenthèses englobant ce qui suit et qui est utilisé pour indiquer l’ordre d’exécution de droite à gauche) :

import Data.Map as M (fromList, map, (!))

paladins count = M.map (*count) $ M.fromList [("attack", 60), ("infDef", 45), ("cavDef", 50), ("magDef", 90), ("artDef", 40)]

Il ne resterait alors plus qu’à rajouter les fonctions d’accès aux propriétés des différentes unités militaires :

attack units = units ! "attack"
infDef units  = units ! "infDef"
-- etc.

Cette approche est probablement celle que l’on adopterait avec un langage à typage dynamique comme, par exemple, Clojure. Or, Haskell permet de jouer avec les types, pourquoi ne pas en profiter pour représenter nos troupes comme formant un seul type :

data Troop = Swordmen Int | Scouts Int | Crossbowmen Int  | Squires Int | Templars Int |  
  CavalryArchers Int | Paladins Int | RoyalKnights Int |  Rams Int | ArcaneMages Int | 
  BattleMages Int | HolyClergies Int | IronGolems Int
  deriving (Show, Read)

Le type Troop définit l’ensemble de ses valeurs possibles, chacune générée par un constructeur (une fonction) qui se trouve ici paramétré par un entier qui, dans notre programme, représentera le nombre d’unités. Pour obtenir des paladins, il suffira juste d’appeler le constructeur idoine avec, en argument, le nombre de paladins que l’on a. Vous remarquerez que l’on a pas défini les propriétés de chacune des valeurs du type. Elles pourront l’être sous la forme d’un jeu de fonctions :

attack :: Troop  -> Int
attack (Paladins count) = 60 * count
attack (RoyalKnights count) = 100 * count
-- etc

Ou :

attack :: Troop -> Int
attack t = case t of
   (Paladins count) -> 60 * count
   (RoyalKnights count) -> 100 * count
   -- etc

Il pourrait être sympathique de pouvoir rassembler ces propriétés dans un même moule et tisser une relation entre lui et notre type représentant les troupes militaires. Justement Haskell supporte les classes de types. Une classe de types est un type de types ; c’est-à-dire qu’elle définit l’ensemble des fonctions que les types doivent implémenter pour appartenir à la classe. Or, justement, ces caractéristiques peuvent ne pas être l’apanage de nos troupes mais caractériser toute unité de combat quel qu’elle soit. Pourquoi ne pas représenter alors les caractéristiques de nos troupes sous une telle forme et dans laquelle chacune des propriétés est définie par une fonction, et faire en sorte que notre type Troop soit un élément de cette classe, autrement dit donne corps à chacune des propriétés de combat :

data FighterClass = Infantry | Cavalry | Artillery | Mage
  deriving (Eq)

class Fighter a where
  kind   :: a -> FighterClass
  attack :: a -> Int
  infDef :: a -> Int
  cavDef :: a -> Int
  magDef :: a -> Int
  artDef :: a -> Int

instance Fighter Troop where
  attack (Swordmen count) = 40 * count
  attack (Scouts count) = 10 * count
  -- etc
  
  infDef (Swordmen count) = 15 * count
  infDef (Scouts count) = 10 * count
  -- etc

Maintenant, il reste à définir l’attaque et la défense d’une armée selon les règles du jeu King’s Empire et en se basant sur notre classe de types :

attackPower :: Fighter a => [a] -> Int
attackPower = foldr (\x y -> attack x + y) 0

defensePower :: Fighter a => Wall -> [a] -> [a] -> Int
defensePower (Wall r) _ [] = r
defensePower (Wall r) def att  = r + (round $ (infantryPercent att) * (infantryPower def) + 
  (cavalryPercent att) * (cavalryPower def) + (magePercent att) * (magicPower def) + 
  (artilleryPercent att) * (artilleryPower def))
  where
    asFloat          i = fromIntegral i :: Float
    attackingPower     = asFloat $ attackPower att
    cumulAttack        = foldr (\x y -> attack x + y) 0
    percentOf        n = (asFloat n) / attackingPower
    infantryPercent  a = percentOf $ cumulAttack $ filter (\x -> kind x == Infantry) a
    cavalryPercent   a = percentOf $ cumulAttack $ filter (\x -> kind x == Cavalry) a
    magePercent      a = percentOf $ cumulAttack $ filter (\x -> kind x == Mage) a
    artilleryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Artillery) a
    infantryPower    a = asFloat $ foldr (\x y -> y + infDef x) 0 a
    cavalryPower     a = asFloat $ foldr (\x y -> y + cavDef x) 0 a
    magicPower       a = asFloat $ foldr (\x y -> y + magDef x) 0 a
    artilleryPower   a = asFloat $ foldr (\x y -> y + artDef x) 0 a

Le calcul des points des armées s’appuie sur la propriété que les unités qui les composent satisfont la classe de type Fighter. Le calcul des points de défenses de l’armée attaquée est plus complexe car d’une part ils s’appuient sur les capacités défensives de la ville (ici les remparts qui entourent la ville et représentée par le type Wall) et d’autre part ils sont conditionnés par les capacités offensives de l’agresseur. Ainsi, par exemple, les points défensifs de l’infanterie du défenseur sont proportionnés aux points d’attaques de l’infanterie présente dans l’armée ennemie. De ces calculs, nous pouvons alors déterminer laquelle des deux armées sera victorieuse :

-- Runs the battle between two armies, whose one is behind the town's protections. 
-- It returns the ratio between the attack power and the defense power:
-- a positive value means the attacker wins whereas a negative value means the defenser wins.
-- More the absolute ratio is high, more damages the lost army are important.
battle :: Fighter a => [a]-> [a] -> Wall -> Int
battle att def wall = (attackPower att) - (defensePower wall def att)

Nous avons donc représenté les différentes unités militaires du jeux King’s Empire comme étant des valeurs du type Troop qui est une instance de la classe de types Fighter. Nous aurions pu très bien, par soucis de simplicité, représenter les troupes comme un simple constructeur des différentes propriétés de combat et définir chacune des unités par le biais d’une fonction :

data TroopType = Infantry | Cavalry | Artillery | Mage
  deriving (Eq, Read, Show)

data Troop = Troop { kind::TroopType, attack::Int, infDef::Int, cavDef::Int, magDef::Int, artDef::Int }
  deriving (Show, Read)

data TroopUnit = Swordmen | Scouts | Crossbowmen  | Squires |  Templars | CavalryArchers | 
  Paladins | RoyalKnights |  Rams  | ArcaneMages  | BattleMages  | HolyClergies | IronGolems 
  deriving (Show, Read)

troopOf :: Int -> TroopUnit -> Troop
troopOf n Paladins = Troop { kind = Cavalry, attack = n * 60, infDef = n * 45, cavDef = n * 50,
  magDef = n * 90, artDef = n * 40 }
-- etc

Dans notre solution, les propriétés sont rassemblées, au sein de la classe de type, par leur nature (attaque, défense en infanterie, etc.) quelque soit l’unité militaire. Dans cette dernière solution, elles sont rassemblées par unité. L’avantage est que les propriétés de chaque unité ne sont plus diluées dans différentes fonctions mais, au contraire, rassemblées au sein d’une seule et leur modification reste alors plus simple et donc moins sujette à l’erreur.

Avec ce billet, vous avez pu constater que les constructions dans un langage fonctionnel peuvent être riches et offrir ainsi plusieurs manières de représenter les concepts d’une application. L’enjeu sera alors d’identifier celle qui facilitera plus facilement l’évolution du code, et en cette matière les techniques de l’Agilité, comme le TDD, peuvent fortement nous aider.


comments powered by Disqus