简体   繁体   English

按类型类组合的类型上的多态函数

[英]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 ,但是您仍然保留ServiceMemberInfoVeteranInfoCivilianInfo的单独记录定义。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM