第 11 章:交互式程序设计
01 交互式程序
到目前为止,我们接触到的绝大多数 Haskell 程序都可以归为 批处理程序 (Batch Program)。
批处理程序是这样一种程序:
- 
它在开始时接收一组参数,然后在结束时返回一个值
- 在执行过程中 (即,开始与结束之间),该程序无法接收外部的参数,也不会向外界输出信息
 
 
与批处理程序相对的,是 交互式程序 (Interactive Program):这类程序在执行过程中能够从键盘读入信息,并向屏幕输出信息。
令人遗憾的是,在 Haskell 中编写交互式程序,是一件相对困难的事情。
- 
Haskell 程序在本质上是数学意义上的 纯函数 (Pure Function)。
- 
从来没有一本数学书或一位数学家说:一个函数可以与键盘或屏幕这种世俗的东西发生交互
 - 
也即,数学中的函数没有 副作用 (Side Effects):即,不会对世界的状态产生直接的改变
 
 - 
 - 
但是,交互式程序具有副作用:即,从键盘读入信息、向屏幕输出信息
 
但是,一件 非常搞笑 的事情是:
- 只要把我们的思维方式稍微调整一下,Haskell 无法处理交互式程序的问题就迎刃而解了
 
任何一个交互式程序都可以为被视为一个纯函数:
- 
该函数的输入参数:世界的当前状态 (The current state of the world)
 - 
该函数的输出参数:改变后的世界 (A modified world)
 
type IO = WORLD -> WORLD
为了将计算结果进行显式化,可以对上述类型定义进行轻微调整:
type IO a = WORLD -> (a, WORLD)
因此,Haskell 中的交互式程序,具有 IO a 这种类型:
- 称这种类型的一个值,是一个返回一个 
a类型值的 动作 (Action) 
唐僧:
- 你不会真的以为,
 IO a把整个世界的状态读入程序吧?小和尚:
- 如果你不说,我们还真这样认为呢 (看,我们多天真🐯)
 
02 Prelude 模块提供的若干 IO 动作
getChar :: IO Char
该动作具有如下行为:
- 
读入用户通过键盘输入的一个字符
 - 
将这个字符输出到屏幕上
 - 
将这个字符作为返回值
 
putChar :: Char -> IO ()
该函数具有如下行为:
- 
接收一个字符
c作为输入参数 - 
返回一个动作:该动作向屏幕输出字符
c,并返回一个零元组() 
return :: a -> IO a
该函数具有如下行为:
- 
接收一个
a类型的参数x - 
返回一个动作:该动作不产生任何副作用,直接返回
x 
注意:千万不要把这个
return函数与其他语言中的return关键字混淆了
Haskell 提供了一个关键字
do,用于表达 “顺序执行若干动作”。例如:
act :: IO (Char, Char) act = do x <- getChar getChar y <- getChar return (x, y)
首先注意到,这个动作的类型是
IO (Char, Char)
- 也即,在执行完动作后,会返回一个类型为
 (Char, Char)的值
do后面顺序放置了 4 个 动作
x <- getChar
执行动作
getChar,并把返回的那个Char值赋到变量x上注意:
getChar的类型是IO Char因此,
x = getChar这种方式无法把IO Char中的Char赋给x这里的
<-,就是 List Comprehension 中的 Generator 中的<-
- 更多细节,在后文中讲解
 
getChar
- 执行
 getChar这个动作,且忽略其返回值
y <- getChar
- 执行动作
 getChar,并把返回的那个Char值赋到变量x上
return (x, y)
return的类型:a -> IO a
从键盘读入一行字符串:
getLine :: IO String
getLine = do x <- getChar
             if x == '\n' then
                 return []
             else
                 do xs <- getLine
                    return (x:xs)
向屏幕输出一个字符串:
putStr :: String -> IO ()
putStr []     = return ()
putStr (x:xs) = do putChar x
                   putStr xs
向屏幕输出一个字符串,并换行:
putStrLn :: String -> IO ()
putStrLn xs = do putStr xs
                 putChar '\n'
一个简单的交互式程序
strlen :: IO ()
strlen = do putStr "Enter a string: "
            xs <- getLine
            putStr "The string has "
            putStr (show (length xs))
            putStrLn " characters"
ghci> strlen
Enter a string: Haskell
The string has 7 characters
03 示例:Hangman 游戏
游戏规则:
- 
玩家一:在键盘上秘密地输入一个单词
secret - 
玩家二:尝试去推理出这个单词。推理过程如下:
- 
玩家二在键盘上输入一个猜测的单词
guess - 
计算机点亮
secret中那些出现在guess中的字母 - 
玩家二跳转到第 1 步,进行新一轮的猜测,直到猜中
 
 - 
 
ghci> hangman
Think of a word:
-------
Try to guess it:
? pascal
-as--ll
? rust
--s----
? haspell
has-ell
? haskell
You got it!
下面,我们采用自顶向下的策略实现这个游戏。
首先给出最顶层的函数:
hangman :: IO ()
hangman = do
    putStrLn "Think of a word: "
    word <- sgetLine  -- get a string secretly
    putStrLn "Try to guess it:"
    play word  -- play the game
其中,动作 sgetLine 的行为:
- 
从键盘上读入一行字符串
secret - 
将其中的每一字母以字符
-输出到屏幕 
sgetLine :: IO String
sgetLine = do
     x <- getCh -- get a char without echoing
     if x == '\n' then
         do putChar x
            return []
     else
         do putChar '-'
            xs <- sgetLine
            return (x:xs)
其中,动作 getCh 的行为:
- 从键盘读入一个字符,但是不把这个字符输出到屏幕
 
import System.IO (hSetEcho, stdin)
getCh :: IO Char
getCh = do
    hSetEcho stdin False
    x <- getChar
    hSetEcho stdin True
    return x
- 这里有一些底层的实现细节,可以不用太关注
 
函数 play 是游戏的主体:支持玩家二不断进行猜测,并输出系统的反馈。
play :: String -> IO ()
play word = do
     putStr "? "
     guess <- getLine
     if guess == word then
         putStrLn "You got it!"
     else
      do putStrLn (match word guess)
         play word
match :: String -> String -> String
match xs ys = [if elem x ys then x else '-' | x <- xs]
04 示例:Nim 游戏
游戏规则:
- 
一个棋盘,初始状态如下:
1: * * * * * 2: * * * * 3: * * * 4: * * 5: * - 
两个玩家轮流对棋盘进行如下操作:
- 选择一行,并从这一行的尾部删除一或多个 
* 
 - 选择一行,并从这一行的尾部删除一或多个 
 - 
清空棋盘的玩家是游戏的赢家
 
下面,我们采用自底向上的策略实现这个游戏。
棋盘的表示和显示:
type Board = [Int]
initial :: Board
initial = [5,4,3,2,1]
finished :: Board -> Bool
finished = all (== 0)
putBoard :: Board -> IO ()
putBoard [a, b, c, d, e] = do
    putRow 1 a
    putRow 2 b
    putRow 3 c
    putRow 4 d
    putRow 5 e
putRow :: Int -> Int -> IO ()
putRow row num = do
    putStr $ show row
    putStr ": "
    putStrLn $ concat $ replicate num "* "
ghci> putBoard initial
1: * * * * *
2: * * * *
3: * * *
4: * *
5: *
游戏中的一次操作:从一行的尾部删除一或多个 *
-- 判断一次操作是否合法
valid :: Board -> Int -> Int -> Bool
valid board row del = board !! (row - 1) >= del
-- (!!) :: [a] -> Int -> a
-- List index (subscript) operator, starting from 0
-- (exported by Prelude)
-- 进行一次操作
move :: Board -> Int -> Int -> Board
move board row del = [ update r n | (r,n) <- zip [1..] board ]
  where
    update r n = if r == row then n - del else n
游戏主函数:
nim :: IO
nim = play initial 1
play :: Board -> Int -> IO ()
play board player =
   do newline
      putBoard board
      newline
      if finished board then
         do putStr "Player "
            putStr $ show $ next player
            putStrLn " wins!!"
      else
         do putStr "Player "
            putStrLn $ show player
            row <- getDigit "Enter a row number: "
            del <- getDigit "Stars to remove: "
            if valid board row del then
               play (move board row del) (next player)
            else
               do newline
                  putStrLn "ERROR: Invalid move"
                  play board player
本章作业
作业 01
定义一个动作
adder :: IO (),它具有如下行为:
- 从键盘读入一个正整数
 n- 从键盘读入
 n个整数 (每个整数一行),然后输出这n个整数的和例如:
ghci> adder How many numbers? 5 1 3 5 7 9 The total is 25