2015-01-27 14 views
8

Per testare le mie capacità in Haskell, ho deciso di realizzare il primo gioco che trovate in Land of Lisp/Realm of Racket. Il gioco "Indovina il mio numero". Il gioco si basa sullo stato mutabile per l'esecuzione, in quanto deve costantemente aggiornare i limiti superiore e inferiore per il programma in base al valore che l'utente sta pensando.Guess My Number, un mal di testa monadica

Va un po 'di qualcosa di simile:

> (guess) 
50 
> (smaller) 
25 
> (bigger) 
37 

Ora, questo genere di cose (a mia conoscenza) non è del tutto possibile in Haskell, chiamando qualche funzione dal REPL che modifica stato mutevole globale, quindi stampa immediatamente il risultato, in quanto viola il principio di immutabilità. Pertanto tutte le interazioni devono vivere all'interno di una monade IO e/o State. Ed è lì che sono bloccato.

Non riesco a spiegarmi come combinare la monade IO e la monade State, così posso ottenere input, stampare risultati e modificare lo stato, il tutto nella stessa funzione.

Ecco quello che ho ottenuto finora:

type Bound = (Int, Int) -- left is the lower bound, right is the upper 

initial :: Bound 
initial = (1, 100) 

guess :: Bound -> Int 
guess (l, u) = (l + u) `div` 2 

smaller :: State Bound() 
smaller = do 
    [email protected](l, _) <- get 
    let newUpper = max l $ pred $ guess bd 
    put $ (l, newUpper) 

bigger :: State Bound() 
bigger = do 
    [email protected](_, u) <- get 
    let newLower = min u $ succ $ guess bd 
    put $ (newLower, u) 

Tutto quello che devo fare ora, è quello di trovare un modo per

  • stampa tentativo iniziale
  • ricevere il comando di voler più piccolo/più grande numero
  • modificare lo stato di conseguenza
  • chiamare la funzione in modo ricorsivo in modo da indovinare di nuovo

Come si combinano IO e State in modo elegante per raggiungere questo obiettivo?

Nota: sono consapevole che questo può probabilmente essere ottenuto senza l'uso dello stato; ma voglio renderlo fedele all'originale

+3

Aspetta, quindi è il computer che indovina il numero? – Shoe

+1

@Jefffrey sì, nella versione Lisp, il computer indovina, non la persona; ecco perché è molto più divertente implementare –

+0

Vedi anche [Gestione dello stato] (http://stackoverflow.com/q/10048213/791604). –

risposta

12

È possibile combinare diverse monadi utilizzando i trasformatori monad - in questo caso StateT. È possibile utilizzare il codice esistente modificando le firme di tipo da utilizzare StateT:

bigger, smaller :: Monad m => StateT Bound m() 

allora si può scrivere una funzione per eseguire il gioco dato un parametro di stato:

game :: StateT Bound IO() 
game = do 
    s <- get 
    liftIO $ print (guess s) 
    verdict <- (liftIO getLine) 
    case verdict of 
    "smaller" -> smaller >> game 
    "bigger" -> bigger >> game 
    "ok" -> return() 
    _ -> (liftIO $ putStrLn $ "Unknown verdict " ++ verdict) >> game 

si utilizza liftIO per sollevare un IO azione nella monade StateT Bound IO, che consente di richiedere input e leggere la riga successiva.

Infine è possibile eseguire il gioco utilizzando runStateT:

runStateT game initial 
+0

molto bello, molto semplice, molto elegante! Questo è esattamente ciò che avevo in mente –

+0

non sarebbe meglio scrivere 'print $ guess s' invece di' putStrLn (show. Guess $ s) '?, la funzione' print' chiama 'show' internamente comunque –

+1

@ ElectricCoffee - Sì, è un buon punto, aggiornato. – Lee

1

Ecco una soluzione che utilizza il trasformatore StateT. Punti notevoli:

  1. Legge l'input dell'utente utilizzando getLine invece di utilizzare REPL.
  2. Si legge molto come un programma imperativo, tranne che è necessario aggiungere liftIO a qualsiasi azione IO.
  3. Si esegue il ciclo con runStateT in cui si fornisce anche lo stato iniziale.

Il programma:

import Control.Monad.State 

loop :: StateT (Int,Int) IO() 
loop = do 
    (lo,hi) <- get 
    let g = div (lo+hi) 2 
    liftIO $ putStrLn $ "I guess " ++ show g 
    ans <- liftIO getLine 
    case ans of 
    "lower" -> do put (lo,g); loop 
    "higher" -> do put (g,hi); loop 
    "exact" -> return() 
    _  -> do liftIO $ putStrLn "huh?"; loop 

main = runStateT loop (0,50) 
+0

Mi piacerebbe usare il mio codice esistente, se possibile ... –

+0

Vuoi dire che vuoi interagire con l'utente tramite il REPL? Non è possibile utilizzare il codice esistente perché una firma come "State Bound()" non consente di eseguire alcun IO e non è possibile eseguire 'larger' e' smaller' dal REPL - non ha alcun senso . Puoi chiamare 'runState larger' dal REPL, ma devi anche fornire lo stato. – ErikR

2

Non c'è bisogno di usare la monade Stato a tutti in questo esempio. Ecco un esempio che passa lo stato come parametro:

loop :: Bound -> IO() 
loop [email protected](l,u) = do 
    putStr "> " 
    line <- getLine 
    case line of 
    "(guess)" -> print (guess bd) >> loop bd 
    "(smaller)" -> do 
    let newUpper = max l $ dec $ guess bd 
    print $ guess (l, newUpper) 
    loop (l, newUpper) 
    "(bigger)" -> do 
    let newLower = min u $ inc $ guess bd 
    print $ guess (newLower, u) 
    loop (newLower, u) 
    "" -> return() 
    _ -> putStrLn "Can't parse input" >> loop bd 

main :: IO() 
main = loop initial 

In caso contrario, il concetto che si sta cercando sono trasformatori monade.Ad esempio utilizzando StateT:

smaller :: StateT Bound IO() 
smaller = do 
    [email protected](l, _) <- get 
    let newUpper = max l $ dec $ guess bd 
    put $ (l, newUpper) 

bigger :: StateT Bound IO() 
bigger = do 
    [email protected](_, u) <- get 
    let newLower = min u $ inc $ guess bd 
    put $ (newLower, u) 

guessM :: StateT Bound IO() 
guessM = get >>= lift . print . guess 

loop :: StateT Bound IO() 
loop = do 
    lift $ putStr "> " 
    line <- lift getLine 
    case line of 
    "(guess)" -> guessM >> loop 
    "(smaller)" -> do 
    smaller 
    guessM 
    loop 
    "(bigger)" -> do 
    bigger 
    guessM 
    loop 
    "" -> return() 
    _ -> lift (putStrLn "Can't parse input") >> loop 

main :: IO() 
main = evalStateT loop initial 

Vedi questo chapter of Real World Haskell per un tutorial sul tema trasformatori monade.

+0

Vorrei poter utilizzare il mio codice esistente, se possibile –

+1

@ElectricCoffee: Cosa succede se il codice esistente è errato? –

+1

Una buona programmazione riguarda la separazione delle preoccupazioni, avendo una funzione che fa tutto rispetto alle funzioni riutilizzabili più piccole è generalmente sempre meglio. Quello che ho adesso si comporta come mi aspetto che sia, sono solo i bit rimanenti che devono essere sistemati –

7

Cosa si chiede è una sorta di possibile ...

import Data.IORef 

makeGame :: IO (IO(), IO(), IO()) 
makeGame = do 
    bound <- newIORef (1, 100) 
    let guess = do 
      (min, max) <- readIORef bound 
      print $ (min + max) `div` 2 

     smaller = do 
      (min, max) <- readIORef bound 
      let mid = (min + max) `div` 2 
      writeIORef bound (min, mid) 
      guess 

     bigger = do 
      (min, max) <- readIORef bound 
      let mid = (min + max) `div` 2 
      writeIORef bound (mid, max) 
      guess 

    return (guess, smaller, bigger) 

Nevermind quanta ridondanza è in quel codice, questo era solo un prova rapida del concetto. Ecco una sessione di esempio:

$ ghci guess.hs 
GHCi, version 7.9.20141202: http://www.haskell.org/ghc/ :? for help 
[1 of 1] Compiling Main    (guess.hs, interpreted) 
Ok, modules loaded: Main. 
*Main> (guess, smaller, bigger) <- makeGame 
*Main> guess 
50 
*Main> smaller 
25 
*Main> bigger 
37 
*Main> 

nidificate IO tipi possono essere divertente e utile.

+0

Questo è un idioma importante, grazie. – luqui