假设您必须定义一个取决于特定决策的过程。例如:
sendMonsterToGraveyard :: Game -> IO Game
sendMonsterToGraveyard game = do
let monsters = monstersInPlay game
monster <- choose monsters
sendToGraveyard game monster
此功能是假想游戏的“效果”,让玩家选择一个怪物杀死。该设计的问题在于我们将效果耦合到IO monad。如果,稍后我们决定我们希望AI使用该效果(并因此选择一个怪物来杀死),该怎么办?除了让AI知道终端之外,这是不可能的,但这听起来并不健全。那么,什么是重新设计这种模式的正确方法,以便可以编码游戏效果而不将其专门与IO monad耦合?
注意:我是根据要求提出此问题作为我上一个问题的后续跟进。其中一个答案实际上为这个问题提供了一个很好的解决方案,但是,由于没有在问题上专门解决,我们认为最好创建一个新的。
答案 0 :(得分:2)
将IO
留在那里并不是那么疯狂;它也是获取随机数的最简单方法。
您需要做的是将choose
(以及需要的任何其他内容)作为参数传递到函数中,以便接受您未指定的参数choose :: Game -> [Monster] -> IO Monster
预先。您甚至可以将它们指定为自己的newtype
:
newtype MonsterChooser = MonsterChooser (Game -> [Monster] -> IO Monster)
userMonsterChooser = MonsterChooser $ \_ monsters -> loopUntilJust $ do
let indices = zip ['a'..'z'] monsters
putStrLn "Choose a monster to target:"
putStrLn $ unlines $ map (\(i, monster) -> i : '.' : ' ' : show monster) indices
index <- getLine
return $ lookup (trim (lowercase index)) indices
loopUntilJust :: (Monad m) => m (Maybe x) -> m x
loopUntilJust mmx = do
mx <- mmx
case mx of Nothing -> loopUntilJust mmx
Just x -> return x
这是一个更复杂的例子,允许你指定一个MonsterChooser。在选择怪物之前,AI会查看整个Game
;用户只被问到他们想做什么。然后sendMonsterToGraveyard :: MonsterChooser -> Game -> IO Monster
足够通用,您无需担心。
答案 1 :(得分:1)
这是使用DSL的好地方。
基本上,您可以定义您的操作并为这些操作编写不同的解释器,您应该得到类似的结果:
chooseMonster :: Game Monster
chooseMonster = do
monsters <- monstersInPlay
monster <- chooseMonster
sendToGraveyard monster
interpretPlayer = interpret game go
where go action = case action of
chooseMonster -> getMonsterFromUser
...
interpretAI = interpret game go
where go action = case action of
chooseMonster -> calculateBestMonster
...
实际上,IO也是一回事,唯一的区别是Haskell的运行时系统会解释IO动作。就像Game
是一项操作一样,interpretPlayer
和interpretAI
是执行这些操作的不同方式。
free和operational软件包提供了一种简单的方法来创建类似这样的DSL。另外,您可以查看https://softwareengineering.stackexchange.com/questions/242795/what-is-the-free-monad-interpreter-pattern。