[英]Polymorphic function over types combined by typeclass
Consider such domain logic: three types of users: Civilians, ServiceMembers and Veterans. 考虑这样的域逻辑:三种类型的用户:平民,ServiceMembers和退伍军人。 Each of them has 'name', stored in different attributes.
它们每个都有“名称”,存储在不同的属性中。
Task is to write a function, accepting each of the types and returning 'C' char for Civilians, 'V' char for Veterans and 'S' char for ServiceMembers. 任务是编写一个函数,接受每种类型,并为“平民”返回“ C”字符,为退伍军人返回“ V”字符,为ServiceMembers返回“ S”字符。
I have such record declarations: 我有这样的记录声明:
data ServiceMemberInfo = ServiceMemberInfo { smname::String }
data VeteranInfo = VeteranInfo { vname::String }
data CivilianInfo = CivilianInfo { cname::String }
My first idea was to combine them by such typeclass: 我的第一个想法是通过此类typeclass组合它们:
class UserLetter a where
userLetter :: a -> Char
And implement instances: 并实现实例:
instance UserLetter ServiceMemberInfo where
userLetter _ = 'S'
instance UserLetter VeteranInfo where
userLetter _ = 'V'
instance UserLetter CivilianInfo where
userLetter _ = 'C'
In this case, userLetter
is a function I wanted. 在这种情况下,
userLetter
是我想要的功能。 But I really would like to write something like that (without typeclasses) 但是我真的很想写这样的东西(没有类型类)
userLetter1 :: UserLetter a => a -> Char
userLetter1 (CivilianInfo _) = 'C'
userLetter1 (ServiceMemberInfo _) = 'S'
userLetter1 (VeteranInfo _) = 'V'
which throws compilation error: 'a' is a rigid type variable bound by 这会引发编译错误:“ a”是由
Another way is to use ADT: 另一种方法是使用ADT:
data UserInfo = ServiceMemberInfo { smname::String }
| VeteranInfo { vname::String }
| CivilianInfo { cname::String }
Then userLetter1 declaration becomes obvious: 然后,userLetter1声明变得显而易见:
userLetter1 :: UserInfo -> Char
userLetter1 (CivilianInfo _) = 'C'
userLetter1 (ServiceMemberInfo _) = 'S'
userLetter1 (VeteranInfo _) = 'V'
But, lets say, I don't have control over ServiceMemberInfo (and others) declarations. 但是,可以说,我无法控制ServiceMemberInfo(和其他)声明。 How userLetter1 can be defined?
如何定义userLetter1?
Is there a way to declare one ADT with existing ServiceMemberInfo (and others) types? 是否可以使用现有ServiceMemberInfo(和其他)类型声明一个ADT?
It is possible to use existing type-classes to do this, and meet the pattern-matching-like syntax requirements you have, by defining a type -level function which returns the appropriate string, then picking the term-level string that corresponds to the type-level one. 通过定义一个返回适当字符串的类型级别函数,然后选择与该类型级别相对应的术语级别字符串,可以使用现有的类型类来做到这一点,并满足您具有类似模式匹配的语法要求。类型一级。 Here's a complete working example:
这是一个完整的工作示例:
{-# 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))
We can see it go in ghci: 我们可以在ghci中看到它:
*Main> label (ServiceMemberInfo "")
"S"
However, there's a lot not to like about this solution: it requires many extensions; 但是,此解决方案有很多令人不喜欢的地方:它需要许多扩展。 it's complicated (hence will be a maintenance problem);
它很复杂(因此将成为维护问题); and it is in some sense done this way only to paper over a design problem in the underlying types, which would be better served by eliminating the technical debt you've incurred already.
从某种意义上讲,这样做只能解决基础类型中的设计问题,而这可以通过消除已经产生的技术债务来更好地解决。
I would just redefine the datatypes like so: 我将像这样重新定义数据类型:
newtype UserInfo = User { type :: UserType, name :: String }
data UserType = Civilian | ServiceMember | Veteran
But if you really can't change the original datatypes, then you can do something like the following with ViewPattern
and optiononally PatternSynonyms
: 但是,如果您确实无法更改原始数据类型,则可以使用
ViewPattern
和Optiononly 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"
This isn't very safe because you can call userLetter
with any Typeable
(which is everything); 这不是很安全,因为您可以使用任何
Typeable
(包括所有内容)调用userLetter
; it could be better (but more work) to define a class like: 定义一个类可能会更好(但需要更多工作):
class Typeable a => UserLetter a
instance UserLetter ServiceMemberInfo
...
“Is there a way to declare one ADT with existing ServiceMemberInfo (and others) types?”
“有没有办法用现有的ServiceMemberInfo(和其他)类型声明一个ADT?”
Why, sure there is! 为什么,肯定有!
data UserInfo = ServiceMemberUserInfo ServiceMemberInfo
| VeteranUserInfo VeteranInfo
| CivilianUserInfo CivilianInfo
Then userLetter1 :: UserInfo -> Char
can be defined as before, but you still keep the seperate record definitions of ServiceMemberInfo
, VeteranInfo
and CivilianInfo
. 然后可以像以前那样定义
userLetter1 :: UserInfo -> Char
,但是您仍然保留ServiceMemberInfo
, VeteranInfo
和CivilianInfo
的单独记录定义。
Instead of declaring this as a new named ADT, you can also make it an “anonymous variant type”: 除了将其声明为新的ADT之外,还可以使其成为“匿名变体类型”:
type (+) = Either
type UserInfo = ServiceMemberInfo + VeteranInfo + CivilianInfo
Then you can define 然后您可以定义
userLetter1 :: UserInfo -> Char
userLetter1 (Left (Left _)) = 'C'
userLetter1 (Left (Right _)) = 'S'
userLetter1 (Right _) = 'V'
Clearly, this is not really preferrable: the anonymous constructors are much less descriptive. 显然,这并不是真正可取的:匿名构造函数的描述性要差得多。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.