考虑这样的域逻辑:三种类型的用户:平民、军人和退伍军人。它们每个都有"name",存储在不同的属性中。
任务是编写一个函数,接受每种类型,并返回平民的"C"字符、退伍军人的"V"字符和军人的"S"字符。
我有这样的记录声明:
data ServiceMemberInfo = ServiceMemberInfo { smname::String }
data VeteranInfo = VeteranInfo { vname::String }
data CivilianInfo = CivilianInfo { cname::String }
我的第一个想法是通过这样的类型类将它们组合起来:
class UserLetter a where
userLetter :: a -> Char
并实现实例:
instance UserLetter ServiceMemberInfo where
userLetter _ = 'S'
instance UserLetter VeteranInfo where
userLetter _ = 'V'
instance UserLetter CivilianInfo where
userLetter _ = 'C'
在这种情况下,userLetter
是我想要的函数。但我真的很想写这样的东西(没有类型类)
userLetter1 :: UserLetter a => a -> Char
userLetter1 (CivilianInfo _) = 'C'
userLetter1 (ServiceMemberInfo _) = 'S'
userLetter1 (VeteranInfo _) = 'V'
引发编译错误:"a"是绑定的刚性类型变量
另一种方法是使用ADT:
data UserInfo = ServiceMemberInfo { smname::String }
| VeteranInfo { vname::String }
| CivilianInfo { cname::String }
然后userLetter1声明变得显而易见:
userLetter1 :: UserInfo -> Char
userLetter1 (CivilianInfo _) = 'C'
userLetter1 (ServiceMemberInfo _) = 'S'
userLetter1 (VeteranInfo _) = 'V'
但是,比方说,我不能控制ServiceMemberInfo(和其他)声明。如何定义userLetter1?
有没有一种方法可以用现有的ServiceMemberInfo(和其他)类型声明一个ADT?
可以使用现有的类型类来实现这一点,并通过定义返回适当字符串的type-level函数,然后选择与类型-level对应的术语-level字符串,来满足模式匹配之类的语法要求。下面是一个完整的工作示例:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
import GHC.TypeLits
import Data.Proxy
data ServiceMemberInfo = ServiceMemberInfo { smname::String }
data VeteranInfo = VeteranInfo { vname::String }
data CivilianInfo = CivilianInfo { cname::String }
type family Label x :: Symbol
type instance Label ServiceMemberInfo = "S"
type instance Label VeteranInfo = "V"
type instance Label CivilianInfo = "C"
label :: forall a. KnownSymbol (Label a) => a -> String
label x = symbolVal (Proxy :: Proxy (Label a))
我们可以在ghci:中看到它
*Main> label (ServiceMemberInfo "")
"S"
然而,这个解决方案有很多不喜欢的地方:它需要许多扩展;它很复杂(因此将是一个维护问题);从某种意义上说,它是通过only的方式来解决底层类型中的设计问题的,通过消除已经产生的技术债务会更好地解决这个问题。
我会重新定义数据类型,如下所示:
newtype UserInfo = User { type :: UserType, name :: String }
data UserType = Civilian | ServiceMember | Veteran
但是,如果您真的无法更改原始数据类型,那么您可以使用ViewPattern
和可选的PatternSynonyms
:执行以下操作
{-# LANGUAGE PatternSynonyms, ViewPatterns, StandaloneDeriving, DeriveDataTypeable #-}
import Data.Typeable
data ServiceMemberInfo = ServiceMemberInfo { smname::String }
data VeteranInfo = VeteranInfo { vname::String }
data CivilianInfo = CivilianInfo { cname::String }
deriving instance Typeable ServiceMemberInfo
deriving instance Typeable VeteranInfo
deriving instance Typeable CivilianInfo
pattern ServiceMemberInfo_ x <- (cast -> Just (ServiceMemberInfo x))
pattern VeteranInfo_ x <- (cast -> Just (VeteranInfo x))
pattern CivilianInfo_ x <- (cast -> Just (CivilianInfo x))
type UserLetter = Typeable
-- without pattern synonyms
userLetter :: UserLetter a => a -> Char
userLetter (cast -> Just (CivilianInfo{})) = 'C'
userLetter (cast -> Just (ServiceMemberInfo{})) = 'S'
userLetter (cast -> Just (VeteranInfo{})) = 'V'
userLetter _ = error "userLetter"
-- with pattern synonyms
userLetter1 :: UserLetter a => a -> Char
userLetter1 (CivilianInfo_ _) = 'C'
userLetter1 (ServiceMemberInfo_ _) = 'S'
userLetter1 (VeteranInfo_ _) = 'V'
userLetter1 _ = error "userLetter"
这不是很安全,因为您可以用任何Typeable
调用userLetter
(这就是一切);定义一个类似的类可能会更好(但需要做更多的工作)
class Typeable a => UserLetter a
instance UserLetter ServiceMemberInfo
...
“有没有一种方法可以用现有的ServiceMemberInfo(和其他)类型声明一个ADT”
为什么,当然有!
data UserInfo = ServiceMemberUserInfo ServiceMemberInfo
| VeteranUserInfo VeteranInfo
| CivilianUserInfo CivilianInfo
然后userLetter1 :: UserInfo -> Char
可以像以前一样定义,但您仍然保留ServiceMemberInfo
、VeteranInfo
和CivilianInfo
的单独记录定义。
您也可以将其作为“匿名变体类型”:
type (+) = Either
type UserInfo = ServiceMemberInfo + VeteranInfo + CivilianInfo
然后你可以定义
userLetter1 :: UserInfo -> Char
userLetter1 (Left (Left _)) = 'C'
userLetter1 (Left (Right _)) = 'S'
userLetter1 (Right _) = 'V'
显然,这并不是真正可取的:匿名构造函数的描述性要低得多。