假设我们有一个商店管理应用程序。它有Customer
个,可以chargeFee()
。但是,它应仅针对活跃的Customer
执行此操作。
我看到这样做的常见方法(Java /伪代码)是这样的:
class Customer {
String name
StatusEnum status // 1=active, 2=inactive
}
// and this is how the customers are charged
for (c:Customer.listByStatus(StatusEnum.1)) {
c.chargeFee()
}
这没关系,但它并不能阻止某人从非活动Customer
收取费用。即使chargeFee()
检查Customer
的状态,也就是运行时错误/事件。
因此,考虑到整个'使非法国家无法代表'的事情,如何设计这个应用程序(例如在Haskell中)?如果有人试图向不活跃的客户收费,我想要编译错误。
我在考虑这样的事情,但我仍然不允许我限制chargeFee
,以便无法向无效的Customer
收取费用。
data CustomerDetails = CustomerDetails { name :: String }
data Customer a = Active a | Inactive a
chargeFee :: Active a -> Int -- this doesn't work, do I need DataKinds?
答案 0 :(得分:6)
你可以用幻像类型完成这样的事情:
module Customer
(CustomerKind(..), Customer, {- note: MkCustomer is not exported -}
makeCustomer, activate, chargeFee) where
data CustomerKind = Active | Inactive
data Customer (x :: CustomerKind) = MkCustomer String
mkCustomer :: String -> Customer Inactive
mkCustomer = MkCustomer
-- perhaps `IO (Customer Active)' or something else
activate :: Customer Inactive -> Maybe (Customer Active)
activate = ...
chargeFee :: Customer Active -> Int
chargeFee = ...
这里activate
将以某种方式确保给定客户可以被激活(并且这样做),从而产生所述活跃客户。但尝试调用chargeFee (mkCustomer ...)
是一种类型错误。
请注意,DataKinds
并非严格要求 - 以下内容相同:
data Active
data Inactive
-- everything else unchanged
通过简单地声明两种类型 - ActiveCustomer
和InactiveCustomer
,可以在没有幻像类型的情况下完成相同的操作 - 但是幻像类型方法允许您编写不关心类型的函数顾客:
customerName :: Customer a -> String
customerName (MkCustomer a) = ...
答案 1 :(得分:2)
基本方法是使用单独的类型
data ActiveCustomer = AC String -- etc.
data InactiveCustomer = IC String -- etc.
data Customer = Active ActiveCustomer | Inactive InactiveCustomer
-- only works on active
chargeFee :: ActiveCustomer -> IO ()
chargeFee (AC n) = putStrLn ("charged: " ++ n)
-- works on anyone
getName :: Customer -> String
getName (Active (AC n)) = n
getName (Inctive (IC n)) = n
这也可以在OOP语言中或多或少地完成:只为活动和非活动客户使用不同的类,可能从公共Customer
接口/超类继承。
使用代数类型,您可以获得封闭世界假设的好处,即没有Customer
的其他子类型,但通常没有其他类型。
更高级的方法是使用GADT。 DataKinds
是可选的,但更好,恕我直言。 (警告:未经测试)
{-# LANGUAGE GADTs, DataKinds #-}
data CustomerType = Active | Inactive
data Customer (t :: CustomerType) where
AC :: String -> Customer Active
IC :: String -> Customer Inactive
-- only works on active
chargeFee :: Customer Active -> IO ()
chargeFee (AC n) = putStrLn ("charged: " ++ n)
-- works on anyone
getName :: Customer any -> String
getName (AC n) = n
getName (IC n) = n
或者,用单例分解标记:
data CustomerType = Active | Inactive
data CustomerTypeSing (t :: CustomerType) where
AC :: CustomerTypeSing Active
IC :: CustomerTypeSing Active
data Customer (t :: CustomerType) where
C :: CustomerTypeSing t -> String -> Customer t
-- only works on active
chargeFee :: Customer Active -> IO ()
chargeFee (C _ n) = putStrLn ("charged: " ++ n)
-- works on anyone
getName :: Customer any -> String
getName (C _ n) = n
-- how to build a new customer
makeActive :: String -> Customer Active
makeActive n = C AC n
答案 2 :(得分:0)
您可以随时为chargeFee
或Maybe
提出非法行为:
Either
答案 3 :(得分:0)
所需要的只是标记具有活动状态的类型。我认为不需要单独的构造函数。这样做很容易:
{-# LANGUAGE GADTs #-}
data Active = Active
data Inactive = Inactive
data Customer a where
Customer :: String -> Int -> Customer a
(p.s。我已在您的数据类型中添加了Int
来代表赠送金额,因此您实际上可以通过某种方式向客户收取费用。)
所以Customer Active
代表"有效"客户,同样Customer Inactive
代表"不活跃"顾客。
然后我们可以"创建"客户喜欢这样:
create :: String -> Int -> Customer a
create = Customer
createByStatus :: a -> String -> Int -> Customer a
createByStatus _ = Customer
创建便利方法很简单:
createActive :: String -> Int -> Customer Active
createActive = create
createInactive :: String -> Int -> Customer Inactive
createInactive = create
请注意,直接使用create
可以创建像Customer Int
这样的愚蠢类型。你有几个选择来阻止它,
a
类型类中对create
设置约束。我稍后会通过选项2。
现在我们可以编写一些方法来处理我们的类型:
getName :: Customer a -> String
getName (Customer name _) = name
getCredit :: Customer a -> Int
getCredit (Customer _ credit) = credit
chargeCustomer :: Customer Active -> Int -> Customer Active
chargeCustomer (Customer name credit) charge = Customer name (credit - charge)
请注意,chargeCustomer
仅适用于活跃客户。否则你会收到类型错误。
现在我要编写一个实用函数castCustomer
。
castCustomer :: Customer a -> Customer b
castCustomer (Customer name credit) = Customer name credit
castCustomer
所做的只是将任何类型的客户转变为任何类型的客户。将此视为C中不安全的演员,您不应将其暴露给您的用户。但是编写其他函数很有用:
setActiveStatus :: statusToCheck -> Customer currentStatus -> Customer statusToCheck
setActiveStatus _ = castCustomer
所以你可以setActiveStatus Inactive customer
,然后你会回来customer
但不活跃。它只使用适用于所有演员表的castCustomer
,但setActiveStatus
自己的类型会适当地限制castCustomer
。
还有这些更简单的实用功能:
当然,现在可以编写便利功能了:
setActive :: (LegalStatus a) => Customer a -> Customer Active
setActive = castCustomer
setInactive :: (LegalStatus a) => Customer a -> Customer Inactive
setInactive = castCustomer
最后,人们可能想要这样的函数:
getByStatus :: b -> Customer a -> Maybe (Customer b)
我们传递状态和客户,如果客户匹配状态,则返回该客户,否则返回Nothing
。
根据类型的不同,我们需要不同的实施方式,因此我们需要一个班级。
我们可以写一个像class GetByStatus a b
这样的类,但问题是任何使用这个类的函数都必须在它们的类型签名约束子句中有一个丑陋的GetByStatus a b
。
所以我们要做一个更简单的课程:
class LegalStatus a where ...
这将有两个实例:
instance LegalStatus Active where ...
instance LegalStatus Inactive where ...
以下是LegalStatus
类的定义:
class LegalStatus a where
get :: (LegalStatus b) => Customer b -> Maybe (Customer a)
getActive :: Customer a -> Maybe (Customer Active)
getInactive :: Customer a -> Maybe (Customer Inactive)
这可能看起来令人困惑,但让我们来看看实例:
instance LegalStatus Active where
get = getActive
getActive = Just . castCustomer
getInactive _ = Nothing
instance LegalStatus Inactive where
get = getInactive
getActive _ = Nothing
getInactive = Just . castCustomer
我们在这里做的是一种面向对象的技术https://en.wikipedia.org/wiki/Double_dispatch
。这意味着我们不会使我们的签名复杂化。我们现在可以像以下一样运作:
getByStatus :: (LegalStatus a, LegalStatus b) => a -> Customer b -> Maybe (Customer a)
getByStatus _ = get
使用这些功能和catMaybe
,它可以相对容易地编写功能,例如,获取客户列表并仅返回活动客户:
getAll :: (LegalStatus a, LegalStatus b) => [Customer a] -> [Customer b]
getAll = catMaybes . map get
getAllByStatus :: (LegalStatus a, LegalStatus b) => a -> [Customer b] -> [Customer a]
getAllByStatus _ = getAll
getAllActive :: (LegalStatus a) => [Customer a] -> [Customer Active]
getAllActive = getAll
getAllInactive :: (LegalStatus a) => [Customer a] -> [Customer Inactive]
getAllInactive = getAll
值得指出魔术getAll
是如何(实际上,Haskell中的许多其他类似函数)。请getAll list
如果您列入活跃客户列表,您只会获得列表中的活跃客户,同样,如果您将其放入非活动客户列表中,您就可以了。 ll只会在列表中获得不活跃的客户。
我将通过以下功能证明这一点,该功能将未知状态的客户列表分为活跃客户列表和非活动客户列表:
splitCustomers :: (LegalStatus a) => [Customer a] -> ([Customer Active], [Customer Inactive])
splitCustomers l = (getAll l, getAll l)
查看splitCustomers
的实现,看起来该对的第一个和第二个元素是相同的。实际上它们看起来完全一样。但他们不是,他们有不同的类型,因此最终调用不同的实例,并可能得到完全不同的结果。
如果你真的想要关闭另外一件事。您可能希望公开类LegalStatus
,因为用户可能希望将其用作类型签名中的约束,但这意味着他们可以编写LegalStatus
的实例。像
instance LegalStatus Int where ...
他们做这件事很愚蠢,但如果你愿意,可以阻止他们。最简单的方法是:
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
type family RestrictLegalStatus a where
RestrictLegalStatus Active = ()
RestrictLegalStatus Inactive = ()
type IsLegalStatus a = (RestrictLegalStatus a ~ ())
class (IsLegalStatus a) => LegalStatus a where ...
任何创建新实例的尝试现在都会失败IsLegalStatus
约束并失败。
此时可能已经过度设计了,你不需要这一切,但是我已经将它包括在内以显示关于类型推断的一些观点:
因此,以下所有代码供您参考:
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
module Main where
import Data.Maybe (catMaybes)
main = return ()
data Active = Active
data Inactive = Inactive
type family RestrictLegalStatus a where
RestrictLegalStatus Active = ()
RestrictLegalStatus Inactive = ()
type IsLegalStatus a = (RestrictLegalStatus a ~ ())
data Customer a where
Customer :: String -> Int -> Customer a
class (IsLegalStatus a) => LegalStatus a where
get :: (LegalStatus b) => Customer b -> Maybe (Customer a)
getActive :: Customer a -> Maybe (Customer Active)
getInactive :: Customer a -> Maybe (Customer Inactive)
instance LegalStatus Active where
get = getActive
getActive = Just . castCustomer
getInactive _ = Nothing
instance LegalStatus Inactive where
get = getInactive
getActive _ = Nothing
getInactive = Just . castCustomer
getByStatus :: (LegalStatus a, LegalStatus b) => a -> Customer b -> Maybe (Customer a)
getByStatus _ = get
create :: String -> Int -> Customer a
create = Customer
createByStatus :: a -> String -> Int -> Customer a
createByStatus _ = Customer
createActive :: String -> Int -> Customer Active
createActive = Customer
createInactive :: String -> Int -> Customer Inactive
createInactive = Customer
getName :: Customer a -> String
getName (Customer name _) = name
getCredit :: Customer a -> Int
getCredit (Customer _ credit) = credit
chargeCustomer :: Customer Active -> Int -> Customer Active
chargeCustomer (Customer name credit) charge = Customer name (credit - charge)
castCustomer :: Customer a -> Customer b
castCustomer (Customer name credit) = Customer name credit
setActiveStatus :: (LegalStatus statusToCheck, LegalStatus currentStatus) => statusToCheck -> Customer currentStatus -> Customer statusToCheck
setActiveStatus _ = castCustomer
setActive :: (LegalStatus a) => Customer a -> Customer Active
setActive = castCustomer
setInactive :: (LegalStatus a) => Customer a -> Customer Inactive
setInactive = castCustomer
getAll :: (LegalStatus a, LegalStatus b) => [Customer a] -> [Customer b]
getAll = catMaybes . map get
getAllByStatus :: (LegalStatus a, LegalStatus b) => a -> [Customer b] -> [Customer a]
getAllByStatus _ = getAll
getAllActive :: (LegalStatus a) => [Customer a] -> [Customer Active]
getAllActive = getAll
getAllInactive :: (LegalStatus a) => [Customer a] -> [Customer Inactive]
getAllInactive = getAll
splitCustomers :: (LegalStatus a) => [Customer a] -> ([Customer Active], [Customer Inactive])
splitCustomers l = (getAll l, getAll l)
修改强>
其他人已指出使用DataKinds
来限制状态。不可否认,这可能比我对班级的限制更清晰。"做法。注意你必须改变一些函数,因为类的参数不再是普通类型而是种类,只有普通类型可以作为函数的参数,你必须在{{1}中包装原始状态函数构造函数。
注意使用DataKind方法,您无法再调用Proxy
...因为getByStatus Active
不再是值,您需要执行以下操作:
Active
但随意定义:
getByStatus (Proxy :: Proxy Active) ...
所以你可以打电话:
active :: Proxy Active
active = Proxy
完整的代码如下。
getByStatus active