Polymorphic function over types combined by typeclass

Consider such domain logic: three types of users: Civilians, ServiceMembers and Veterans. 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.

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:

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. 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

Another way is to use ADT:

data UserInfo = ServiceMemberInfo { smname::String }
              | VeteranInfo { vname::String }
              | CivilianInfo { cname::String }

Then userLetter1 declaration becomes obvious:

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. How userLetter1 can be defined?

Is there a way to declare one ADT with existing ServiceMemberInfo (and others) types?


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:

*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 :

{-# 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); 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?”

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 .

Instead of declaring this as a new named ADT, you can also make it an “anonymous variant type”:

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.

链接地址: http://www.djcxy.com/p/43538.html

上一篇: 从一系列较小的类推出一般的类型类实例?

下一篇: 类型结合类型的多态函数