使用类型强制正确

时间:2016-09-16 12:03:04

标签: haskell

假设我们有一个商店管理应用程序。它有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?

4 个答案:

答案 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 

通过简单地声明两种类型 - ActiveCustomerInactiveCustomer,可以在没有幻像类型的情况下完成相同的操作 - 但是幻像类型方法允许您编写不关心类型的函数顾客:

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)

您可以随时为chargeFeeMaybe提出非法行为:

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这样的愚蠢类型。你有几个选择来阻止它,

  1. 仅向用户公开便捷方法
  2. a类型类中对create设置约束。
  3. 我稍后会通过选项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