我正在为在线游戏编写客户端UI.它被构造为表示游戏状态的Model模块和View模块,它跟踪当前游戏状态并使用模型转换(即从一个状态到另一个状态的功能)更新它.为了利用静态类型检查,我将状态建模为具有表示共同特征的类型类的不同类型:
class Erring s where errors :: s -> [String]
class WithPlayers s where players :: s -> [String]
class Erring s => LoggedIn s
data LoggedOut = LoggedOut [String] deriving (Eq, Show)
instance Erring LoggedOut where errors (LoggedOut es) = es
data Ready = Ready [String] [String] deriving (Eq, Show)
instance Erring Ready where errors (Ready _ es) = es
instance LoggedIn Ready
instance WithPlayers Ready where players (Ready ps _) = ps
data NotReady = NotReady [String] [String] deriving (Eq, Show)
instance Erring NotReady where errors (NotReady _ es) = es
instance LoggedIn NotReady
instance WithPlayers NotReady where players (NotReady ps _) = ps
-- some transitions:
login :: String -> LoggedOut -> Either Ready LoggedOut
login pwd (LoggedOut es) =
if pwd == "password" then Left $ Ready [] es
else Right $ LoggedOut (es ++ ["incorrect password"])
logout :: LoggedIn s => s -> LoggedOut
logout s = LoggedOut $ errors s
Run Code Online (Sandbox Code Playgroud)
当有许多状态和实例要定义但产生强大的API时,这可能有点单调乏味.
进入视图.为了存储我想要使用的状态TMVar,所以UI线程和来自服务器的线程处理消息都可以执行状态转换.由于每个状态是不同的类型,我创建了一个可以代表每个可能状态的新类型:
data SessionState = SSLoggedOut LoggedOut
| SSReady Ready
| SSNotReady NotReady
Run Code Online (Sandbox Code Playgroud)
现在可以定义类型的状态引用TMVar SessionState.
现在,这感觉不太对劲.我实际上必须将每个状态定义两次,一次作为类型,另一次作为包装此类型的类型构造函数.所以,问题:
TMVar不同线程的原子更新或是否有更好的方法来跟踪状态?TMVar是正确的方法,是否有必要SessionState定义包装器?我花了一分钟才明白为什么数据LoggedIn是类LoggedOut,但是......
TVar据我所知这是最好的方法。我假设你知道组合器atomically如果您想要类型检查和同步 TMVar,那么您需要定义类型(用于类型检查)和数据包装器(用于 TMVar)。我看不出有什么办法可以解决这个问题;据我所知,TMVar 必须保持相同的类型。(如我错了请纠正我!)
如果是我,我会放弃类型,而使用函数和防护。
data SessionState = Ready {errors :: [String], players :: [String]}
| NotReady {errors :: [String], players :: [String]}
| LoggedOut {errors :: [String]}
deriving (Eq, Show, Ord)
loggedIn :: SessionState -> Bool
loggedIn (LoggedOut _) = False
loggedIn _ = True
ready :: SessionState -> Bool
ready (Ready _ _) = True
ready _ = False
addError :: SessionState -> String -> SessionState
addError s e = s {errors = e:errors s}
addPlayer :: SessionState -> String -> SessionState
addPlayer s@(LoggedOut _) p = addError s $ "Can't add " ++ p ++ " when logged out"
addPlayer s p = s {players = p:players s}
Run Code Online (Sandbox Code Playgroud)
您可以使用以下一些简单的函数从一种状态转移到另一种状态。我尝试给出使用防护和使用模式匹配的示例;你可以选择你更喜欢的风格,或者像我一样混合使用:
login :: SessionState -> SessionState
login (LoggedOut es) = NotReady es []
login s = addError s "Can't log in when already logged in"
logout :: SessionState -> SessionState
logout s
| loggedIn s = LoggedOut $ errors s
| otherwise = addError s "Can't log out when not logged in"
enable :: SessionState -> SessionState
enable (NotReady es ps) = Ready es ps
enable s@(LoggedOut _) = addError s "Can't enable when logged out"
enable s@(Ready _ _ ) = addError s "Can't enable when already ready"
disable :: SessionState -> SessionState
disable s
| ready s = NotReady (errors s) (players s)
| otherwise = addError s "Can't disable when not ready"
Run Code Online (Sandbox Code Playgroud)
还有一个使用该loggedIn函数的愚蠢示例函数:
countPlayers :: SessionState -> (SessionState, Maybe Int)
countPlayers s
| loggedIn s = (s, Just . length $ players s)
| otherwise = (addError s "Can't count players whilst logged out", Nothing)
Run Code Online (Sandbox Code Playgroud)
这种方法通过编译器的类型安全性较低,但仍然具有很强的可读性,而且还有一个额外的好处,那就是灵活。这是我在 ghci 中摆弄的:
*Main> LoggedOut []
LoggedOut {errors = []}
*Main> login it
NotReady {errors = [], players = []}
*Main> enable it
Ready {errors = [], players = []}
*Main> addError it "Illegal somethingorother"
Ready {errors = ["Illegal somethingorother"], players = []}
*Main> logout it
LoggedOut {errors = ["Illegal somethingorother"]}
*Main> disable it
LoggedOut {errors = ["Can't disable when not ready","Illegal somethingorother"]}
Run Code Online (Sandbox Code Playgroud)