根据@Li-yao_Xia 的回答,可以做到这一点GHC.Generics
(这是generic-lens
在幕后使用的)。中的代码generic-lens
可能有点难以理解,所以这里是你可以从头开始的方法。
工作方式GHC.Generics
,它代表一个特定的类型,例如:
data MyProduct = MyProduct String Int Bool deriving (Generic)
通过Rep MyProduct
看起来像这样的同构类型:
> :kind! Rep MyProduct
Rep MyProduct :: * -> *
= D1
('MetaData "MyProduct" "GenericFetch3" "main" 'False)
(C1
('MetaCons "MyProduct" 'PrefixI 'False)
(S1
('MetaSel
'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
(Rec0 String)
:*: (S1
('MetaSel
'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
(Rec0 Int)
:*: S1
('MetaSel
'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
(Rec0 Bool))))
诚然,这有点疯狂,但这种嵌套类型的大部分都包含由 、 和 类型表示的元数据D1
包装C1
器S1
。如果您删除这些包装器,则归结为:
Rep MyProduct = Rec0 String :*: Rec0 Int :*: Rec0 Bool
这有助于显示表示的结构。
无论如何,要编写一个通用函数,您需要创建一个类型类,它可以处理Rep a
使用实例来处理元数据包装器和用于表示产品、总和等的一小组类型构造函数。
在我们的例子中,我们将定义一个类型类Fetch'
,它允许我们从表示中获取类型的第一个值b
(t
即,so t
will beRep MyProduct
或类似的东西):
class Fetch' t b where
fetch' :: t p -> Maybe b
目前,我们不会要求它t
实际包含 a b
,这就是我们允许fetch'
return的原因Nothing
。
我们需要一个实例来处理元数据:
instance Fetch' t b => Fetch' (M1 i m t) b where
fetch' (M1 x) = fetch' x
由于所有元数据包装器(D1
、S1
和C1
)实际上都是别名(M1 D
、M1 S
,分别),我们可以使用通过包装器的实例M1 C
来处理它们。M1
fetch'
我们还需要一个来处理产品:
instance (Fetch' s b, Fetch' t b) => Fetch' (s :*: t) b where
fetch' (s :*: t) = fetch' s <|> fetch' t
这只会b
从产品的左侧取出,或者 - 失败 - 从右侧取出。
我们需要一个实例来b
从匹配类型的(顶级)字段中获取一个(与Rec0
上面的匹配,因为这只是 的别名K1 R
):
instance Fetch' (K1 i b) b where
fetch' (K1 x) = Just x
以及一个重叠的包罗万象,将处理错误类型的字段:
instance {-# OVERLAPPABLE #-} Fetch' (K1 i b) a where
fetch' (K1 _) = Nothing
我们还可以选择在这些表示形式中处理其他可能的类型构造函数(即 、V1
和U1
):+:
,我在下面的完整示例中已经完成了这些。
无论如何,有了这些实例,我们可以编写:
fetch1 :: (Generic t, Fetch' (Rep t) b) => t -> b
fetch1 = fromJust . fetch' . from
这很好用:
> fetch1 prod :: String
"yes"
> fetch1 prod :: Int
0
> fetch1 prod :: Bool
False
但与@luqui 基于Data
泛型的答案一样,它不会在编译时捕获错误字段,而是在运行时崩溃:
> fetch1 prod :: Double
*** Exception: Maybe.fromJust: Nothing
为了解决这个问题,我们可以引入一个类型族来计算数据结构(或者更确切地说是它Rep
)是否实际上包含所需的字段,如下所示:
type family Has t b where
Has (s :*: t) b = Or (Has s b) (Has t b)
Has (K1 i b) b = 'True
Has (K1 i a) b = 'False
Has (M1 i m t) b = Has t b
使用类型 family 的通常定义Or
。现在,我们可以将其作为约束添加到 的定义中fetch
:
fetch :: ( Generic t
, Has (Rep t) b ~ 'True
, Fetch' (Rep t) b)
=> t -> b
fetch = fromJust . fetch' . from
现在我们得到一个错误字段的编译时错误:
> fetch prod :: String
"yes"
> fetch prod :: Double
<interactive>:83:1: error:
• Couldn't match type ‘'False’ with ‘'True’
arising from a use of ‘fetch’
• In the expression: fetch prod :: Double
In an equation for ‘it’: it = fetch prod :: Double
>
无论如何,将整个事情放在一起,并Has
为所有构造函数添加实例和定义,我们得到以下版本。请注意,对于 sum 类型(即(:+:)
),它只允许可以在 sum 的所有项中找到的字段类型(因此保证存在)。与 中的typed
函数不同generic-lens
,此版本允许产品中有多个目标类型的字段,并且只选择第一个。
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleContexts #-}
module GenericFetch where
import Control.Applicative
import Data.Maybe
import GHC.Generics
data MyProduct = MyProduct String Int Bool deriving (Generic)
prod :: MyProduct
prod = MyProduct "yes" 0 False
data AnotherProduct = AP (Maybe Int) Char deriving (Generic)
ap :: AnotherProduct
ap = AP Nothing 'C'
data ASum = A Int String | B Int Double deriving (Generic)
asum :: ASum
asum = A 10 "hello"
class Fetch' t b where
fetch' :: t p -> Maybe b
instance Fetch' V1 b where
fetch' _ = Nothing
instance Fetch' U1 b where
fetch' _ = Nothing
instance (Fetch' s b, Fetch' t b) => Fetch' (s :+: t) b where
fetch' (L1 s) = fetch' s
fetch' (R1 t) = fetch' t
instance (Fetch' s b, Fetch' t b) => Fetch' (s :*: t) b where
fetch' (s :*: t) = fetch' s <|> fetch' t
instance Fetch' (K1 i b) b where
fetch' (K1 x) = Just x
instance {-# OVERLAPPABLE #-} Fetch' (K1 i b) a where
fetch' (K1 _) = Nothing
instance Fetch' t b => Fetch' (M1 i m t) b where
fetch' (M1 x) = fetch' x
type family Has t b where
Has V1 b = 'False
Has U1 b = 'False
Has (s :+: t) b = And (Has s b) (Has t b)
Has (s :*: t) b = Or (Has s b) (Has t b)
Has (K1 i b) b = 'True
Has (K1 i a) b = 'False
Has (M1 i m t) b = Has t b
type family Or a b where
Or 'False 'False = 'False
Or a b = 'True
type family And a b where
And 'True 'True = 'True
And a b = 'False
fetch :: ( Generic t
, Has (Rep t) b ~ 'True
, Fetch' (Rep t) b)
=> t -> b
fetch = fromJust . fetch' . from
给予:
> :l GenericFetch
> fetch prod :: Int
0
> fetch prod :: Double
...type error...
> fetch ap :: Maybe Int
Nothing
> fetch ap :: Int
...type error...
> fetch asum :: Int
10
> fetch asum :: String
... type error: no string in `B` constructor...
>