haskell - Where 子句的最佳实践

标签 haskell design-patterns functional-programming where-clause

我用 Haskell 编写了一个简单的井字游戏程序。它在命令行上运行,具有单人和两人模式,并在与它对战时实现极小极大算法。

我习惯于用面向对象语言编写正确的代码,但 Haskell 对我来说是新的。这段代码工作得相当好,但似乎很难阅读(即使对我来说也是如此!)。关于如何使这段代码更加...Haskelian 有什么建议吗?

import Data.List
import Data.Char
import Data.Maybe
import Control.Monad

data Square = A | B | C | D | E | F | G | H | I | X | O deriving (Read, Eq, Ord)
instance Show Square where
   show A = "a" 
   show B = "b" 
   show C = "c" 
   show D = "d" 
   show E = "e" 
   show F = "f" 
   show G = "g" 
   show H = "h" 
   show I = "i" 
   show X = "X" 
   show O = "O"
type Row = [Square]
type Board = [Row]
data Player = PX | PO deriving (Read, Eq)
instance Show Player where
   show PX = "Player X"
   show PO = "Player O"
data Result = XWin | Tie | OWin deriving (Read, Show, Eq, Ord) 

main :: IO ()
main = do
    putStrLn "Let's play some tic tac toe!!!"
    putStrLn "Yeeeaaaaaahh!!!"
    gameSelect

gameSelect :: IO ()
gameSelect = do
    putStrLn "Who gonna play, one playa or two??? (Enter 1 or 2)"
    gameMode <- getLine
    case gameMode of "1" -> onePlayerMode
                     "2" -> twoPlayerMode
                     gameMode -> gameSelect
    where onePlayerMode = do
             putStrLn "One playa"
             putStrLn "Cool!  Get ready to play...AGAINST MY INVINCIBLE TIC TAC TOE AI!!!!! HAHAHAHA!!!"
             gameLoop 1 emptyBoard PX
          twoPlayerMode = do
             putStrLn "Two players"
             gameLoop 2 emptyBoard PX
          emptyBoard = [[A,B,C],[D,E,F],[G,H,I]]

gameLoop :: Int -> Board -> Player -> IO ()
gameLoop noOfPlayers board player = do
    case detectWin board of Just XWin -> endgame board XWin
                            Just OWin -> endgame board OWin
                            Just Tie -> endgame board Tie
                            Nothing -> if noOfPlayers == 1
                                       then if player == PX 
                                            then enterMove 1 board player 
                                            else enterBestMove board PO
                                       else enterMove 2 board player

enterMove :: Int -> Board -> Player -> IO () 
enterMove noOfPlayers board player = do
     displayBoard board
     if noOfPlayers == 1
     then do putStrLn ("Make your move. (A-I)")
     else do putStrLn (show player ++ ", it's your turn. (A-I)")
     move <- getLine
     print move
     if not $ move `elem` ["a","b","c","d","e","f","g","h","i"]
         then do
            putStrLn $ move ++ " is not a move, doofus"
            gameLoop noOfPlayers board player
         else if (read (map toUpper move) :: Square) `elem` [ sq | sq <- concat board]
            then do
               gameLoop noOfPlayers (newBoard (read (map toUpper move) :: Square) player board) (if player == PX then PO else PX)
            else do
               putStrLn "That square is already occupied"
               gameLoop noOfPlayers board player

enterBestMove :: Board -> Player -> IO ()
enterBestMove board player = gameLoop 1 (newBoard bestmove player board) PX
    where bestmove = fst $ findBestMove PO board
          findBestMove :: Player -> Board -> (Square, Result)
          findBestMove player board
            | player == PO = findMax results
            | player == PX = findMin results
            where findMin = foldl1 (\ acc x -> if snd x < snd acc then x else acc)
                  findMax = foldl1 (\ acc x -> if snd x > snd acc then x else acc)
                  results = [ (sq, getResult b) | (sq, b) <- boards player board ]
                  getResult b = if detectWin b == Nothing 
                                then snd (findBestMove (if player == PX then PO else PX) b) 
                                else fromJust $ detectWin b
                  boards :: Player -> Board -> [(Square, Board)]
                  boards player board = [(sq, newBoard sq player board) | sq <- concat board, sq /= X, sq /=O]

displayBoard :: Board -> IO ()
displayBoard board = do
    mapM_ print board

newBoard :: Square -> Player -> Board -> Board
newBoard move player board = [ [if sq == move then mark else sq | sq <- row] | row <- board]
    where mark = if player == PX then X else O

detectWin :: Board -> (Maybe Result)
detectWin board
   | [X,X,X] `elem` board ++ transpose board = Just XWin
   | [X,X,X] `elem` [diagonal1 board, diagonal2 board] = Just XWin
   | [O,O,O] `elem` board ++ transpose board = Just OWin
   | [O,O,O] `elem` [diagonal1 board, diagonal2 board] = Just OWin
   | [X,X,X,X,X,O,O,O,O] == (sort $ concat board) = Just Tie
   | otherwise = Nothing
   where
     diagonal1 :: Board -> [Square]
     diagonal1 bs = bs!!0!!0 : bs!!1!!1 : bs!!2!!2 : []
     diagonal2 :: Board -> [Square]
     diagonal2 bs = bs!!0!!2 : bs!!1!!1 : bs!!2!!0 : []

endgame :: Board -> Result -> IO ()
endgame board result = do
    displayBoard board
    if result `elem` [XWin, OWin]
        then 
            let player = if result == XWin then PX else PO
            in do 
                putStrLn ("The game is over, and " ++ show player ++ " wins!")
                putStrLn ((if player == PX then show PO else show PX) ++ " is a loser lol")
        else do
            putStrLn "The game is a tie"
            putStrLn "You are both losers!  Ugh!"
    putStrLn "Want to play again? (y/n)"
    again <- getLine
    if again `elem` ["y", "Y", "yes", "Yes", "YES"] 
        then gameSelect 
        else do
            putStrLn "Goodbye"
<小时/>

编辑:特别感谢@Chi 和@Caridorc,我做了以下更改。进一步的建议也将被考虑和更新

import Data.List
import Data.Char
import Data.Maybe
import Control.Monad

data Square = A | B | C | D | E | F | G | H | I | X | O deriving (Read, Eq, Ord)
instance Show Square where
   show A = "a" 
   show B = "b" 
   show C = "c" 
   show D = "d" 
   show E = "e" 
   show F = "f" 
   show G = "g" 
   show H = "h" 
   show I = "i" 
   show X = "X" 
   show O = "O"
type Row = [Square]
type Board = [Row]
data Player = PX | PO deriving (Read, Eq)
instance Show Player where
   show PX = "Player X"
   show PO = "Player O"
data Result = XWin | Tie | OWin deriving (Read, Show, Eq, Ord) 

main :: IO ()
main = do
    putStrLn "Let's play some tic tac toe!!!"
    putStrLn "Yeeeaaaaaahh!!!"
    gameSelect

gameSelect :: IO ()
gameSelect = do
    putStrLn "Who gonna play, one playa or two??? (Enter 1 or 2)"
    gameMode <- getLine
    case gameMode of 
      "1" -> onePlayerMode
      "2" -> twoPlayerMode
      _ -> gameSelect
    where onePlayerMode = do
             putStrLn "One playa"
             putStrLn "Cool!  Get ready to play...AGAINST MY INVINCIBLE TIC TAC TOE AI!!!!! HAHAHAHA!!!"
             gameLoop 1 emptyBoard PX
          twoPlayerMode = do
             putStrLn "Two players"
             gameLoop 2 emptyBoard PX
          emptyBoard = [[A,B,C],[D,E,F],[G,H,I]]

displayBoard :: Board -> IO ()
displayBoard board = do
    mapM_ print board

otherPlayer :: Player -> Player
otherPlayer PX = PO
otherPlayer PO = PX

gameLoop :: Int -> Board -> Player -> IO ()
gameLoop noOfPlayers board player = do
    case detectWin board of 
      Just res -> endgame board res
      Nothing -> case noOfPlayers of
                   1 -> case player of
                          PX -> enterMove 1 board player
                          PO -> enterBestMove board PO 
                   2 -> enterMove 2 board player

enterMove :: Int -> Board -> Player -> IO () 
enterMove noOfPlayers board player = do
     displayBoard board
     case noOfPlayers of
       1 -> do putStrLn ("Make your move. (A-I)")
       2 -> do putStrLn (show player ++ ", it's your turn. (A-I)")
     move <- getLine
     print move
     if not $ move `elem` ["a","b","c","d","e","f","g","h","i"] then do
        putStrLn $ move ++ " is not a move, doofus"
        gameLoop noOfPlayers board player
     else if (read (map toUpper move) :: Square) `elem` (concat board) then do
            gameLoop noOfPlayers (newBoard (read (map toUpper move) :: Square) player board) (otherPlayer player)
          else do
            putStrLn "That square is already occupied"
            gameLoop noOfPlayers board player

enterBestMove :: Board -> Player -> IO ()
enterBestMove board player = gameLoop 1 (newBoard bestmove player board) PX
    where bestmove = fst $ findBestMove PO board

findBestMove :: Player -> Board -> (Square, Result)  -- minimax algorithm
findBestMove player board
  | player == PO = findMax results
  | player == PX = findMin results
  where findMin = foldl1 (\ acc x -> if snd x < snd acc then x else acc)     
        findMax = foldl1 (\ acc x -> if snd x > snd acc then x else acc)
        results = [ (sq, getResult b) | (sq, b) <- boards player board ]
        getResult b = case detectWin b of
                        Nothing -> snd (findBestMove (otherPlayer player) b)
                        Just x -> x
        boards :: Player -> Board -> [(Square, Board)]
        boards player board = [(sq, newBoard sq player board) | sq <- concat board, sq /= X, sq /=O]

newBoard :: Square -> Player -> Board -> Board
newBoard move player board = [ [if sq == move then mark else sq | sq <- row] | row <- board]
    where mark = if player == PX then X else O

detectWin :: Board -> (Maybe Result)
detectWin board
   | [X,X,X] `elem` (triplets board) = Just XWin
   | [O,O,O] `elem` (triplets board) = Just OWin
   | [X,X,X,X,X,O,O,O,O] == (sort $ concat board) = Just Tie
   | otherwise = Nothing

triplets :: Board -> [[Square]]
triplets board = board ++ transpose board ++ [diagonal1] ++ [diagonal2]
   where
     flat = concat board
     diagonal1 = [flat !! 0, flat !! 4, flat !! 8]
     diagonal2 = [flat !! 2, flat !! 4, flat !! 6]

endgame :: Board -> Result -> IO ()
endgame board result = do
    displayBoard board

    putStrLn $ endGameMessage result

    putStrLn "Want to play again? (y/n)"
    again <- getLine
    if again `elem` ["y", "Y", "yes", "Yes", "YES"] 
    then gameSelect 
    else do
        putStrLn "Goodbye"

endGameMessage :: Result -> String
endGameMessage result
   | result `elem` [XWin, OWin] = winnerNotice ++ loserNotice
   | otherwise = "The game is a tie\n" ++ "You are both losers!  Ugh!"
   where
     winner = case result of
      XWin -> PX
      OWin -> PO
     winnerNotice = "The game is over, and " ++ show winner ++ " wins!\n"
     loserNotice = (show $ otherPlayer winner) ++ " is a loser lol"

最佳答案

代码风格通常是个人喜好的问题,在 Haskell 中可以说比其他具有“标准”风格指南的语言更重要。尽管如此,这里还是有一些随机的建议。

<小时/>

不要过度缩进case:只需使用另一行

case gameMode of "1" -> onePlayerMode
                 "2" -> twoPlayerMode
                 gameMode -> gameSelect

对比

case gameMode of
   "1" -> onePlayerMode
   "2" -> twoPlayerMode
   gameMode -> gameSelect

甚至

case gameMode of
   "1" -> onePlayerMode
   "2" -> twoPlayerMode
   _   -> gameSelect
<小时/>

case 通常优先于 if .. == Constructor:

if player == PX 
then enterMove 1 board player 
else enterBestMove board PO

对比

case player of
   PX -> enterMove 1 board player 
   PY -> enterBestMove board PO
<小时/>

我强烈建议不要使用像 fromJust 这样的部分函数,​​因为如果您忘记事先检查 Nothing ,它们可能会导致您的程序崩溃。存在更安全的替代方案,它们永远不会导致此类崩溃——减轻程序员的负担。

if detectWin b == Nothing 
then snd (findBestMove (if player == PX then PO else PX) b) 
else fromJust $ detectWin b

对比

case detectWin b of
   Nothing -> snd $ findBestMove (if player == PX then PO else PX) b
   Just x  -> x

fromMaybe (snd $ findBestMove (if player == PX then PO else PX) b)
  $ detectWin b
<小时/>

尝试分解常用函数。例如

nextPlayer PX = PO
nextPlayer PO = PX

可以替代使用

if player == PX then PO else PX
<小时/>

当只有一条语句时,不需要 do:

if noOfPlayers == 1
then do putStrLn ("Make your move. (A-I)")    -- no need for parentheses here
else do putStrLn (show player ++ ", it's your turn. (A-I)")
<小时/>

既然您在标题中提到了 where,那么我要声明的是,总的来说,我对 where 的感受很复杂。我知道我经常倾向于避免 where 而支持 let,但这种感觉并不与许多其他 Haskeller 一样,所以要小心一点。

就我个人而言,我倾向于将 where 的使用限制为单行:

foo = f x y
   where x = ...
         y = ...

特别是在可能跨越多行的do block 中,我更喜欢lets:

foo = do
   line
   line using x     -- what is x ??!?
   line
   ...
   line
  where x = ...     -- ah, here it is

对比

foo = do
   line
   let x = ...
   line using x
   line
   ...
   line

但是,请随意采用您认为更具可读性的样式。

<小时/>

另外,不要忘记添加一些评论,正如@mawalker 指出的那样。有些定义是显而易见的,不需要任何解释。其他人可以从几行解释目的中受益。

关于haskell - Where 子句的最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34663638/

相关文章:

Haskell 代码中充斥着 TVar 操作和函数,带有许多参数 : code smell?

haskell - 如何在 Proxy 上进行模式匹配?

haskell 错误: "No instance fo (Eq Time) arising from a use of ' =='

windows - 使用 regex-compat 的 Windows 上的简单正则表达式替换崩溃

functional-programming - 主管和工作进程是否应该注册为全局进程? -Erlang

.net - "Iron"语言准备好迎接黄金时段了吗?

java - 使用子类的字段执行父方法

oop - GoF 的 Lexi Editor 应用程序的功能实现(例如在 Haskell 中)

C++ 对象构造函数数组

haskell - 简化 boolean 表达式的函数