2015-01-27 9 views
8

Чтобы проверить свои навыки в Haskell, я решил, что я хотел бы реализовать самую первую игру, вы найдете в Стране Лисп/Сферы Ракетки. "Guess My Number" игра. Игра полагается на изменяемое состояние для запуска, так как она постоянно должна обновлять верхнюю и нижнюю границы программы для дома в зависимости от значения, о котором думает пользователь.Guess My Number, монадическая головной боль

Это идет немного что-то вроде этого:

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

Теперь, такого рода вещи (насколько мне известно) не совсем возможно в Haskell, вызывая некоторую функцию от РЕПЛ, которая изменяет глобального изменяемого состояния, затем печатает результат сразу после, поскольку он нарушает принцип неизменности. Поэтому все взаимодействия должны жить внутри монады IO и/или State. И вот где я застрял.

Возможно, я не могу объединить мой мозг с объединением монады IO и монады State, поэтому я могу получить вход, распечатать результаты и изменить состояние, все в той же функции.

Вот что я получил до сих пор:

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) 

Все, что мне нужно сделать сейчас, чтобы разработать способ

  • печати начального предположения
  • получить команду желающих меньше/больше число
  • изменить состояние соответственно
  • вызывать функцию рекурсивно, чтобы потом угадать

Как скомбинировать IO и State элегантным способом достижения этого?

Примечание: я знаю, что это, вероятно, может быть достигнуто без использования состояния вообще; но я хочу, чтобы это оставалось верным оригиналу

+3

Подождите, так это компьютер, который догадывается номер? – Shoe

+1

@Jefffrey да, в версии Lisp, компьютер догадывается, а не человек; поэтому гораздо интереснее реализовать –

+0

См. также [Управление состоянием] (http://stackoverflow.com/q/10048213/791604). –

ответ

12

Вы можете комбинировать различные монады с использованием монадных трансформаторов - в данном случае StateT. Вы можете использовать существующий код, изменив сигнатуры типов использовать StateT:

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

, то вы можете написать функцию, чтобы запустить игру учитывая состояние параметра:

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 

вы используете liftIO, чтобы поднять IO действие в монаде StateT Bound IO, позволяющее запрашивать ввод и читать следующую строку.

Наконец, вы можете запустить игру с помощью runStateT:

runStateT game initial 
+0

очень красивый, очень простой, очень элегантный! Это именно то, что я имел в виду –

+0

, не лучше ли писать 'print $ guess s' вместо' putStrLn (show. Guess $ s) '?, функция' print' вызывает 'show' внутренне в любом случае –

+1

@ ElectricCoffee - Да, это хороший момент, обновленный. – Lee

1

Вот решение с использованием трансформатора StateT. Известные пункты:

  1. Он считывает ввод пользователя с использованием getLine вместо использования REPL.
  2. Он читается очень как настоятельная программа, за исключением того, что вы должны добавить liftIO в любые действия ввода-вывода.
  3. Вы запускаете цикл с runStateT, где вы также указываете начальное состояние.

Программа:

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

Я бы хотел использовать мой существующий код, если это возможно ... –

+0

Вы хотите, чтобы вы взаимодействовали с пользователем через REPL? Вы не можете использовать свой существующий код, потому что подпись, подобная 'State Bound()', не позволяет выполнять IO, и вы не можете запускать «больше» и «меньше» из REPL - это не имеет никакого смысла , Вы можете вызывать 'runState больше 'из REPL, но тогда вам также нужно предоставить состояние. – ErikR

2

Вы не должны использовать государственные монады вообще в этом примере. Вот пример, который передает состояние в качестве параметра:

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 

В противном случае концепция вы ищете являются монада трансформаторов.Например, используя 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 

Смотрите эту chapter of Real World Haskell для учебника по теме монадных трансформаторов.

+0

Я бы хотел использовать мой существующий код, если возможно –

+1

@ElectricCoffee: Что, если ваш существующий код не так? –

+1

Хорошее программирование - все о разделении проблем, имея одну функцию, которая делает все по сравнению с меньшими функциями многократного использования, как правило, всегда лучше. То, что я имею сейчас, ведет себя так, как я ожидаю, это просто оставшиеся биты, которые должны встать на свои места. –

7

Что вы спросите своего рода возможно ...

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, сколько избыточность в этом коде, это было просто быстрое доказательство концепции. Вот пример сеанса:

$ 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> 

Вложенные IO типа может быть интересно и полезно.

+0

Это важная идиома, спасибо. – luqui