Power GO contro il povero Haskell

Non specifico i dettagli dell’esercizio, dando per scontato che c’eravate o abbiate letto i due articoli precedenti.

Haskell usato come lo uso io (senza lenses) e` particolarmente verboso nella specifica del cambiamento di stato di un oggetto, cosa che e` invece alla base di un linguaggio imperativo. Qua per esempio sto togliendo health ad un giocatore dell’arena

   attacked2 
     = attacked { combatant_health 
                    = max 0 ((combatant_health attacked) - attackStrenght) 
                }

Questa linea dice:

  • il nuovo oggetto “attacked2”, e` come l’oggetto “attacked” iniziale (di cui non posso cambiare niente, dato che in Haskell non posso modificare il valore di una variabile), tranne che ha il campo “health” diverso
  • il campo health e` uguale al massimo fra 0 (quindi non puo` diventare mai negativo) e la salute iniziale (“combatant_health attacked”) meno la potenza dell’attacco

In un linguaggio imperativo, sarebbe stato tutto molto piu` immediato e leggibile, tipo:

attacked.health -= attackStrenght;
if attacked.health < 0 then attacked.health = 0;

Quindi GO segna il primo GOL e si porta sul 1-0.

In Haskell pero` il codice e` piu` componibile, dato che essendoci meno side-effects ci sono anche meno sorprese quando si iniziano ad incastrare mattoncini su mattoncini. Coi pero` non si vincono le partite e quindi siamo ancora 1-0 per GO e il suo team.

Questo codice decide cosa fare, quando il giocatore “attacker” si scontra contro “attacked”

attack attacker attacked moves1 
  = case attacker == attacked of
      True
        -> moves1
           -- non fa niente e torna le mosse di prima
      False
        -> case combatant_isDead attacker of
             True
               -> (DeadMove attacker):moves1
                  -- un giocatore morto non puo` attaccare
             False
                -> case (mod tick (combatant_attackSpeed attacker) == 0) of
                    False
                      -> moves1
                         -- non e` ancora il turno di attacker e non puo` fare mosse
                    True
                      -> case combatant_isDead attacked of
                           True -> moves1
                                   -- un giocatore morto non puo` essere attaccato
                           False -> (AttackMove (combatant_name attacker) 
                                                (combatant_name attacked)
                                    ):moves1
                                   -- aggiunge una mossa di attacco alla lista di mosse fatte

In questo caso il codice funzionale ricorda un po’ quelle dimostrazioni matematiche in cui si analizzano le diverse path possibili, e terminano con un “come volevasi dimostrare”. E` elegante, ma di certo non posso segnare nessun gol a favore, dato che sono semplici azioni di contenimento a centro campo.

In Haskell si applica una mossa all’arena in questo modo:

arena_move :: Arena -> Move -> Arena 

cioe` si definisce una funzione che accetta un’arena iniziale, la mossa da applicare, e torna la nuova arena con la mossa applicata. Siccome non si puo’ cambiare il valore di una variabile, non si puo` che tornare un nuovo ogetto.

Perlomeno l’applicazione di piu` mosse ad una arena e` compatta come codice:

arena_moves :: Arena -> [Move] -> Arena
arena_moves arena1 moves = foldl' arena_move arena1 moves

e iniziamo a capire perche` le funzioni si compongono velocemente in Haskell. La “foldl” applica iterativamente una funzione di move all’arena precedente, passando come parametro la move successiva presa nella lista di mosse da applicare. Nel caso di due giocatori vive sono solo 2 mosse, o nessuna se sono tutti e due morti o non e` il loro turno, o piu` mosse se si stanno scontrando diversi giocatori.

I parser in Haskell si scrivono velocemente, dato che una delle librerie di Haskell (Parsec) definisce un interprete di un Domain Specific Language, adatto per descrivere i parser. E` tipico in Haskell descrivere computazioni complesse, usando una notazione ad-hoc (Domain Specific Language) interpretata da una libreria. Quindi Haskell da un linguaggio verboso, diventa un linguaggio compatto e ad-hoc per risolvere il problema di turno. Quello che in un linguaggio OO diventa una pattern (una serie di ogetti organizzati in un modo ricorrente), in Haskell diventa un interprete, e una rappresentazione piu` compatta del codice, usando un Domain Specific Language. Poi nella realta` gli interpreti hanno i loro limiti: meglio un aproccio a compilatore, dove e` possibile fare una pre-analisi del tutto, per esempio e` difficile in Haskell dire se la grammatica contiene errori di definizione o ricorsioni infinite, cose che invece riesce a fare un parser compilato con Bison/YACC & C.

parseLine :: Parser Combatant
parseLine = do 
   cn <- parseString
   sep
   speed <- parseInt
   sep
   health <- parseInt
   sep
   wn <- parseString
   sep
   wStrenght <- parseWeaponStrenght

   let w = Weapon {
             weapon_name = wn
           , weapon_strength = wStrenght
           }

   let c = Combatant {
             combatant_name = cn
           , combatant_attackSpeed = speed
           , combatant_health = health
           , combatant_weapon = w
           , combatant_order = 0          
           }

   return c

Qua` e` chiaramente un gol a favore di Haskell, ma peccato che sia stato annullato dall’arbitro per fuori gioco, dato che il team GO ha saggiamente optato per usare direttamente una libreria CSV del linguaggio, invece che scriversi il proprio parser.

Questo e` il codice per fare il parsing delle armi con potenza casuale all’interno di un range

parseWeaponStrenght :: Parser (Int, Int)
parseWeaponStrenght
  = P.try (do strictSpace
              P.char '('
              strictSpace
              i1 <- parseInt
              P.char ','
              strictSpace
              i2 <- parseInt
              P.char ')'
              strictSpace
            return (i1, i2)) <|> (do i <- parseInt
                                     return (i, i))

riconosce stringhe come “1” oppure “(1, 2)”.

Il simbolo “<|>” usato nel codice sopra dice: se il primo parser non e` riuscito a fare match, allora prova con questa alternativa. Il tutto codificato come libreria del linguaggio, non come built-in. Notevole direi. Quindi 1-1.

Ma purtroppo questo e` stato l’ultimo goal del team Haskell e segnato al limite del tempo regolamentare, quando oramai il team GO aveva dilagato segnando diversi goal e completando tutti gli esercizi del incremental Kata, e per di piu` giocando fuori casa, in completa discomfort zone, con un linguaggio nuovo sotto tutti i punti di vista. Praticamente la squadra primavera del GO ha annientato il blasonato ma viziato team Haskell.

L’allenatore del team Haskell ha provato a protestare affermando che il codice di Haskell supportava RPG fra 3 o piu` giocatori, e non solo 2, ma l’arbitro ha fatto notare che mai nel corso del Incremental Kata era stato chiesto questo e ha quindi ammonito il team Haskell, per mancanza di rispetto delle regole del TDD.

Il mio giudizio su Haskell: il linguaggio del futuro sara` un linguaggio con molte idee prese dai linguaggi funzionali (come gia` accade, vedi i Nullable types), una semantica/sintassi piu` ad oggetti tipo “subject.property” e la gestione dei domain-specific-languages attraveros una fase di analisi/compilazione, invece che di interpretazione. Quindi non sara` Haskell cosi` come e` ora a mio parere. Pero` molte delle idee dei linguaggi funzionali stanno diventando main-stream nei linguaggi imperativi tradizionali.

Ma voglio terminare “fantasticando” su cosa poteva fare l’accoppiata Roberto / Alessandro / Python: una manciata di chiamate di libreria, due piccole funzioni come collante e il pitone con le batterie termonucleari incluse, avrebbe strangolato nella sua morsa il povero Haskell ancor prima del fischio di inizio 🙂

This entry was posted in code kata. Bookmark the permalink.

3 Responses to Power GO contro il povero Haskell

  1. rgambuzzi says:

    In Python non viene tanto più corto dai. http://pastebin.com/RjBXKvA0

    Pero’ ti faccio notare la riga del ‘parser’.


    players = [Pawn(re.split(r"\s,(?![^(]))\s", line)) for line in open('combat.rpg').read().splitlines()]

    • Massimo Zaniboni says:

      La tua linea di codice Python e` sicuramente efficacie, ma rischia una multa dalla buon costume, per chiamate oscene di features private, su oggetti pubblici 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax