给出一个求和类型:
type PlayerId = String
data Location = Deck | Hand PlayerId
我该如何编写这两个函数(我不在乎采用哪种通用方法……可以帮助我找出更合适的奖励积分):
myF :: Generic a => a -> [String]
myF :: Data a => a -> [String]
-- roughly equivalent to
myF :: Location -> [String]
myF x = case x of
Deck -> ["deck"]
Hand pid -> ["hand", show pid]
(对于任何“无效”类型,例如,该参数不可Show
,请返回[]
或error
。)
上下文:我想为它们通用地定义Data.Aeson.ToJSON
实例,但有许多类似的枚举类型,尽管上面给出了myF
我知道如何做其余的事情。尽管大多数情况下,我只是这样做是为了学习有关通用编程的更多信息。
尝试:
使用Generic
λ> unM1 $ from Deck
(L1 (M1 {unM1 = U1}))
λ> :t (undefined :: Rep Location p)
(undefined :: Rep Location p)
:: D1
('MetaData "Location" "Test" "main" 'False)
(
C1 ('MetaCons "Deck" 'PrefixI 'False) U1
:+:
C1
('MetaCons "Hand" 'PrefixI 'False)
(S1
('MetaSel
'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
(Rec0 String)))
由于:+:
被定义为L1 | R1
,因此我可能可以“合并”以上两个结果。我不确定我会这样做的一种好方法..也许在前者上进行模式匹配,并使用它“降级”到后者-但我不确定如何在类型定义和真实代码。
使用Data
AFAICT Data
是泛型的另一种方法。您可以使用 Generic
或 Data
,对吗?
我认为我需要使用gmap*
函数之一,但是我不知道如何将类型与我的问题联系起来。尝试了一些探索性的“将随机参数插入各种方法”,但没有发现任何有趣的地方。
更新!我试图简化我的示例,但是我做得太多了。在我的实际代码中,PlayerId
是围绕字符串的新类型。在这种情况下,以下“有效”(以小写形式表示构造函数名称):
mkQ :: (Typeable a, Typeable b) => r -> (b -> r) -> a -> r
(r `mkQ` q) a = case cast a of
Just b -> q b
Nothing -> r
myF :: Data a => a -> [String]
myF input =
[showConstr . toConstr $ input]
++ gmapQ (\x -> ("" `mkQ` f) x) input
f :: PlayerId -> String
f (PlayerId x) = x
这里的见解是构造函数和参数需要区别对待。另一个问题是上述代码需要了解PlayerId
。以下内容无效:
f :: Show a => a -> String
f = show
...,因为它与gmapQ
的类型签名不匹配。我想我理解为什么会这样:gmapQ的工作方式是使用cast
,而f
的定义还不够具体,无法为其提供实际的类型。我不确定是否可以解决此问题,或者是否限制使用Data
。 (不过,即使不是理想情况,这仍然可能是可行的:我可以想象这样一种情况:我有myF
由某些fs
参数化了,这些参数特定于类型中的特定参数。)
感觉也不对,因为我从original SYB paper复制了mkQ
函数...我以为我应该能够使用Data.Data
提供的函数来做到这一点。
答案 0 :(得分:2)
使用泛型时,您不需要合并两种类型的信息。您所需要做的就是通过实例处理每种可能的类型。
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeOperators #-}
import GHC.Generics
type PlayerId = String
data Location = Deck | Hand PlayerId deriving Generic
instance MyClass Location
class MyClass a where
myF :: a -> [String]
default myF :: (Generic a, MyClass1 (Rep a)) => a -> [String]
myF = defaultMyF
defaultMyF :: (Generic a, MyClass1 (Rep a)) => a -> [String]
defaultMyF a = myF1 $ from a
Rep a
具有种类* -> *
,所以我们不能直接为MyClass
,U1
,V1
等实现M1
。相反,我们需要另一个类其中myF
的类型为:: a b -> [String]
。
class MyClass1 a where
myF1 :: a b -> [String]
instance MyClass1 V1 where
myF1 _ = []
instance MyClass1 U1 where
myF1 _ = []
instance MyClass1 (K1 i String) where
myF1 (K1 a) = [a]
instance (MyClass1 f, Constructor t) => MyClass1 (C1 t f) where
myF1 c@(M1 a) = (conName c) : myF1 a
instance (MyClass1 f) => MyClass1 (D1 t f) where
myF1 (M1 a) = myF1 a
instance (MyClass1 f) => MyClass1 (S1 t f) where
myF1 (M1 a) = myF1 a
instance (MyClass1 a, MyClass1 b) => MyClass1 (a :+: b) where
myF1 (L1 x) = myF1 x
myF1 (R1 x) = myF1 x
instance (MyClass1 a, MyClass1 b) => MyClass1 (a :*: b) where
myF1 (a :*: b) = myF1 a ++ myF1 b
现在您可以对其进行测试:
main :: IO ()
main = do
putStrLn $ show $ myF Deck
putStrLn $ show $ myF $ Hand "1234"
答案 1 :(得分:2)
这是使用generics-sop的解决方案。
{-# LANGUAGE DeriveAnyClass, DeriveGeneric, FlexibleContexts, ScopedTypeVariables, TypeApplications #-}
import Data.Char
import Generics.SOP
import qualified GHC.Generics as GHC
type PlayerId = String
data Location = Deck | Hand PlayerId
deriving (GHC.Generic, Generic, HasDatatypeInfo)
该库使用自己的通用表示形式,该通用表示形式可以从GHC Generic
类自动导出,也可以通过Template Haskell导出。我们使用前一种方法,这意味着我们必须通过GHC.Generic
扩展来导出DeriveGeneric
,然后通过Generic
扩展来获取SOP的HasDatatypeInfo
和DeriveAnyClass
类。 / p>
我们现在分两步进行。第一个只是获取值的构造函数的名称为小写字符串(因为这是您在示例中使用的名称)。此函数的变体确实应该在库中,但是不幸的是,它不是,所以我们必须自己定义它:
lcConstructor :: forall a . (Generic a, HasDatatypeInfo a) => a -> String
lcConstructor x =
hcollapse
(hzipWith
(\ c _ -> K (map toLower (constructorName c)))
(constructorInfo (datatypeInfo (Proxy @a)))
(unSOP (from x))
)
本质上,constructorInfo (datatypeInfo (Proxy @a))
构造一个表,其中包含类型a
的所有构造函数信息。然后,对hzipWith
的调用从表中选择正确的组件(与所讨论的值x
对应的组件)。此外,我们从构造函数信息中提取名称,并将其转换为小写字符。
我们可以测试这部分:
GHCi> lcConstructor Deck
"deck"
GHCi> lcConstructor (Hand "42")
"hand"
剩下的工作是获取所有构造函数参数的字符串表示形式,并将它们附加到构造函数名称之后:
myF :: (Generic a, HasDatatypeInfo a, All2 Show (Code a)) => a -> [String]
myF a =
(lcConstructor a :) . hcollapse . hcmap (Proxy @Show) (mapIK show) . from $ a
在这里,from
将值转换为其表示形式,然后hcmap
使用show
将构造函数的所有参数转换为字符串,然后hcollapse
将结果提取为字符串列表,并在(lcConstructor :)
前面加上构造函数的名称。
GHCi> myF Deck
["deck"]
GHCi> myF (Hand "42")
["hand", "\"42\""]