我正在为在线游戏编写客户端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
当有许多状态和实例要定义但产生强大的API时,这可能有点单调乏味。
输入视图。为了存储我想要使用TMVar
的状态,以便UI线程和来自服务器的线程处理消息都可以执行状态转换。由于每个州都是不同的类型,因此我创建了一个可以代表每种可能状态的新类型:
data SessionState = SSLoggedOut LoggedOut
| SSReady Ready
| SSNotReady NotReady
现在可以定义类型TMVar SessionState
的状态引用。
现在,这感觉不太对劲。我实际上必须将每个状态定义两次,一次作为类型,另一次作为包装此类型的类型构造函数。所以,问题:
TMVar
是否合理?TMVar
是正确的方式,那么是否需要定义类似SessionState
包装器的内容?答案 0 :(得分:3)
当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}
以下是一些可能用于从一种状态移动到另一种状态的简单函数。我试图给出使用警卫和使用模式匹配的例子;你可以选择你喜欢哪种风格或者像我一样混音:
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"
使用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)
这种方法通过编译器具有较少的类型安全性,但仍然可以非常易读,并且作为额外的好处,灵活。这是我在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"]}