我和一个学院在haskell的博弈论中实现了一个仿真器--迭代囚徒困境。我们希望得到关于代码质量的任何反馈,以及如何更有效或更优雅地解决这些问题。
该代码文件连同一个tex文件(以及已编译的PDF)以识字的haskell风格解释了问题,所有的代码都可以找到论GitHub。
我在这里也包括了代码本身:
{-# LANGUAGE FlexibleInstances #-}
module Main where
import System.Random
import Data.List (nubBy, sortBy, intercalate)
import Data.Bifunctor (bimap)
import Data.Function (on)
main :: IO ()
main = do
result <- startSimulation 100 3 100
print $ show $ stats $ snd result
data Choice = Cooperate | Defect
deriving (Eq, Show)
type BattleResult = (Choice, Choice)
-------------------------- TYPES --------------------------
type PlayerID = Int
type Payment = Int
type PlayerHist = [((Choice, Payment), (PlayerID, Choice))]
data Player = Player
{ name :: String -- strategy name
, playerID :: PlayerID
, decide :: PlayerHist -> Choice
, getPlayerHist :: PlayerHist
}
instance Show Player where
show (Player n p _ o) =
"Player { name: '" ++ n ++ "'" ++
", playerID: " ++ (show p) ++
", getPlayerHist: " ++ (show o) ++ "'}"
instance Eq Player where
(Player n _ _ _) == (Player n' _ _ _) = n == n'
instance Eq (Int -> Player) where
p1 == p2 = (p1 0) == (p2 0)
instance Show (Int -> Player) where
show p = show $ p 0
type Population = [Player]
type RandList = [Int]
type IterationResult = [Player]
-------------------------- DEFINITIONS --------------------------
payment :: BattleResult -> (Int, Int)
payment (Cooperate, Cooperate) = (3,3)
payment (Cooperate, Defect) = (1,4)
payment (Defect, Cooperate) = (4,1)
payment (Defect, Defect) = (2,2)
defector :: Int -> Player
defector n = Player
"Defector"
n
(\_ -> Defect)
[]
cooperator :: Int -> Player
cooperator n = Player
"Cooperator"
n
(\_ -> Cooperate)
[]
tftDecide :: PlayerHist -> Choice
tftDecide [] = Cooperate
tftDecide ((_,(_,c)):_) = c
tft :: Int -> Player
tft n = Player
"TFT"
n
tftDecide
[]
rageDecide :: PlayerHist -> Choice
rageDecide [] = Cooperate
rageDecide l = if (elem Defect . map getOpChoice $ l) then Defect else Cooperate
where getOpChoice = snd . snd
rage :: Int -> Player
rage n = Player
"Yasha"
n
rageDecide
[]
playerTypes :: [Int -> Player]
playerTypes = [defector, cooperator, tft, rage]
generatePopulation :: [(Int->Player, Int)] -> Population
generatePopulation = map (\(i,p) -> p i) .
zip [1..] .
intercalate [] .
map (\(p,n) -> replicate n p)
-------------------------- GAME LOGIC --------------------------
-- shuffled population iteration count
runIteration :: Population -> Int -> IterationResult
runIteration p i = undoPairs $ play i (makePairs p)
-- counter shuffled list of battles
play :: Int -> [(Player, Player)] -> [(Player, Player)]
play 0 h = h
play i p
| i < 0 = p
| otherwise = play (i-1) $ newPlayers decisions
where
dec p = decide p $ getPlayerHist p
decisions = zip p $ map (bimap dec dec) p :: [((Player, Player), BattleResult)]
newPlayers =
map (\((p1,p2),cs@(c1,c2)) ->
let (a1, a2) = payment cs
in
(p1{getPlayerHist = ((c1, a1),(playerID p2, c2)):(getPlayerHist p1)}
,p2{getPlayerHist = ((c2, a2),(playerID p1, c1)):(getPlayerHist p2)}))
-- tournaments maxIterations initial Population for shuffling stats for tournaments with updated histories
runGame :: Int -> Int -> ([[(Int->Player, Int)]], Population) -> RandList -> ([[(Int->Player, Int)]], Population)
runGame _ maxIter res [] = res
runGame 0 maxIter res _ = res
runGame i maxIter res@(hist,ps) rs@(h:t)
| i < 0 = res
| otherwise = runGame (i-1) maxIter (iterStats:hist, newPopulation) $
drop (length iteration) t
where
getPayments = map (snd . fst) . getPlayerHist :: Player -> [Payment]
iteration = runIteration (shuffle rs ps) maxIter :: Population
iterStats = map (\p -> (p, sum .
map (sum . getPayments) .
filter (==(p 0)) $ iteration)
) playerTypes :: [(Int->Player, Payment)]
payments = sum . map snd $ iterStats :: Int
newPopulationStats = map (\(p, s) -> (p, calcCount s payments (length ps))) iterStats :: [(Int->Player, Payment)]
newPopulation = generatePopulation newPopulationStats :: [Player]
startSimulation :: Int -> Int -> Int -> IO ([[(Int->Player, Int)]], Population)
startSimulation genSize tournaments iterations = do
g <- getStdGen
let gen = generatePopulation $ map (\p-> (p, genSize `div` (length playerTypes))) playerTypes
randList = randoms g
putStrLn "Simulating Iterated prisoner"
putStrLn $ "Population " ++ show (stats gen)
return $ runGame tournaments iterations ([], gen) randList
-------------------------- AUXILIARY --------------------------
shuffle :: RandList -> [a] -> [a]
shuffle rands xs = let
ys = take (length xs) rands
in
map fst $ sortBy (compare `on` snd) (zip xs ys)
makePairs :: [a] -> [(a,a)]
makePairs [] = []
makePairs [_] = []
makePairs (h:h':t) = (h,h'):(makePairs t)
undoPairs :: [(a,a)] -> [a]
undoPairs [] = []
undoPairs ((a,b):t) = [a,b]++(undoPairs t)
stats :: Population -> [(String, Int)]
stats l = map (\p -> (name p, length $ filter (\e->name e == name p) l)) $
nubBy (\p1 p2 -> name p1 == name p2) l
-- tries to preserve the calculated amount for each player as close as possible
-- player payout overall payout population size
calcCount :: Int -> Int -> Int -> Int
calcCount _ 0 _ = 0
calcCount _ _ 0 = 0
calcCount a g p = let a' = fromIntegral a
g' = fromIntegral g
p' = fromIntegral p
in round $ a'/g'*p' 发布于 2019-12-15 01:44:52
总的来说,我认为您在分离关注点和保持功能独立方面做得很好。我遇到的一个问题是,有太多的元组而不是记录类型,所以有时您要处理的事情并不明显,例如在getPayments = map (snd . fst) . getPlayerHist中,如果不是snd . fst,而是getPayment . getFirstTuple,那就更好了。要做到这一点,您可以替换:
type PlayerHist = [((Choice, Payment), (PlayerID, Choice))]使用
data FirstTuple = FirstTuple
{ getChoice :: Choice
, getPayment :: Payment
}
data SecondTuple = SecondTuple
{ getOpponentID :: PlayerID
, getOpponentChoice :: Choice
}
data GameResult = GameResult
{ getFirstTuple :: FirstTuple
, getSecondTuple :: SecondTuple
}
type PlayerHist = [GameResult]最好有更多的描述性的名字,如果可能的话。如果处理嵌套类型变得过于复杂,可以考虑使用lens来简化这一点。
在几个地方,您使用[(Int->Player, Int)]来表示球员列表,其中第二个元组项目是每个玩家的计数,第一个项目获得一个ID并返回一个播放器。我认为您可以像使用表示一样容易地使用[Player],并让调用者首先负责调用generatePopulation。这将简化某些类型的意义,并使其更易于阅读,特别是因为它不清楚[(Int->Player, Int)]是什么。
在generatePopulation函数中,在列表中使用intercalate [] . map,这相当于concatMap,这应该更容易理解。请注意,以下类型是等价的:
Prelude Data.List> :t \f -> intercalate [] . map f
\f -> intercalate [] . map f :: (a1 -> [a2]) -> [a1] -> [a2]
Prelude Data.List> :t \f -> concatMap f
\f -> concatMap f :: Foldable t => (a -> [b]) -> t a -> [b]在相同的函数中,您还使用map和zip,with与zipWith相同。
这就是我想出来的:
generatePopulation :: [(Int->Player, Int)] -> Population
generatePopulation = zipWith (flip ($)) [1..] .
concatMap (\(f, count) -> replicate count f)在shuffle中,您可以用ys = zipWith const rands xs代替ys = take (length xs) rands。这应该在1次遍历中做同样的事情,而不是2次。const被定义为const a b = a,所以当您将这两个元素压缩在一起时,您将只从第一个列表rands中获取元素。当较短的列表耗尽时,zip就停止了,所以剩下的长度是length xs。您可以看到一些类似的这里。示例。
https://codereview.stackexchange.com/questions/233969
复制相似问题