Where子句的最佳实践

时间:2016-01-07 19:33:47

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

我在Haskell写了一个简单的tic-tac-toe程序。它在命令行上运行,具有一个和两个播放器模式,并在你对抗它时实现一个minimax算法。

我习惯用OO语言编写适当的代码,但Haskell对我来说是新手。这段代码工作得相当好,但似乎很难读(甚至对我来说!)。关于如何使这个代码更多的任何建议... Haskellian?

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"

1 个答案:

答案 0 :(得分:14)

代码风格通常是个人偏好的问题,在Haskell中可以说比其他语言更具有“标准”风格指南。不过,这里有一些随机的建议。

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

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

VS

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

VS

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

VS

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块中,我更喜欢let s:

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

VS

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

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

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