我想计算特定类别的数据类型的“ arity”。即具有单个构造函数和一定数量字段的数据类型。例如。 data T a = T Int () String a
。然后,“ arity”将是字段数。对于T a
,它将为4
。我设想了一个带有如下签名的函数:
forall a . C a => Int
选择C
。我知道如果我有Generic a
的某个类型a
,我会得到from :: a -> Rep a x
,但是请注意,这将需要a
的具体值,我有兴趣对其进行计算静态地。这有可能吗?我还考虑过Typeable
,但我不太了解API。
答案 0 :(得分:7)
我们可以使用泛型。在整个答案中使用了很多扩展,这些扩展对于各种元编程都很常见。我将首次提及它们,但有关更多详细信息,请参阅其他资源,例如GHC用户指南(list of extensions) 或Haskell Wiki。
data T = T Int Bool String deriving Generic
-- Used extension: DeriveGeneric
派生实例包括Rep
的类型家族实例,该实例构造了T
类型的通用表示。 Rep T
使用在the GHC.Generics
module中发现的一组固定类型:
type Rep T = M1 D _ ((M1 C _ (K1 _ Int) :*: M1 C _ (K1 _ Bool)) :*: M1 C _ (K1 _ String))
--
-- Irrelevant details hidden in underscores.
-- There's actually a few more M1's as well
--
-- You can see the full and real details in a ghci session with this command
-- :kind! Rep T
我们将定义一个类型级别的函数来检查该结构并计算字段数。这是它的签名:
type family Arity (f :: Type -> Type) :: Nat
-- If T is a type with one constructor (C x1 ... xn),
-- Arity (Rep T) is the arity n of that constructor
-- Used extensions: TypeFamilies, DataKinds
在泛型表示时,我们可以假设TT = (Type->Type)
就像是具有以下构造函数的ADT:
-- We can pretend that there is this data type TT
-- such that Arity is a function (TT -> Nat)
data TT
= M1 Type Meta TT
| (:+:) TT TT
| V1
| (:*:) TT TT
| U1
| K1 Type Type
非常(也是?)简短概述。 M1
包含诸如类型名称(包括模块和包),构造函数名称,构造函数是否使用记录符号,字段严格性等信息。V1
和(:+:)
用于类型零个或多个构造函数,因此它们与我们无关。 U1
代表无效的构造函数,而(:*:)
分割n元的构造函数,并在任一侧代表一半字段。 K1
标记了一个构造函数字段。
我们通过为函数Arity
指定类型实例来定义函数。但是,实际上,首先了解一下,不用理会type instance
关键字,而是假装Arity
是像往常一样由模式匹配定义的功能。
看看上面的表示法Rep T
,我们首先遇到一个M1
节点,我们将其忽略并对其内容进行递归调用Arity
。
type instance Arity (M1 i c f) = Arity f
然后我们看到(:*:)
将一组字段分为两部分;我们递归地计算它们的arities并将它们加起来。
type instance Arity (f :*: g) = Arity f + Arity g
-- Used extensions: TypeOperators, UndecidableInstances
U1
代表无效构造函数,
type instance Arity U1 = 0
和K1
是一个字段。
type instance (K1 i a) = 1
现在,在给定通用类型T
(即具有Generic
实例)的情况下,Arity (Rep T)
是其类型类型Nat
。在ghci中,我们可以使用
:kind! Arity (Rep T)
使用GHC.TypeNats.natVal
将其转换为Natural
值(类似于Integer
,但非负)。
-- Calculate the arity of the constructor of a generic type `a`.
-- `a` must have a single constructor.
arity :: forall a. (Generic a, KnownNat (Arity (Rep a))) => Natural
arity = natVal (Proxy @(Arity (Rep a)))
-- Used extensions:
-- ScopedTypeVariables,
-- AllowAmbiguousTypes, TypeApplications,
-- FlexibleContexts
我们将任何通用类型T
的Arity作为值arity @T
,例如可以使用fromIntegral :: Natural -> Integer
进行转换。
main = print (arity @T)
要点:https://gist.github.com/Lysxia/10f1da354f051b2d2eb24f6aace1bf9c
答案 1 :(得分:5)
要在评论中回答我的问题,下面是一个示例,说明如何查找函数的稀疏性。
{-# LANGUAGE ScopedTypeVariables, FlexibleInstances #-}
import Data.Proxy
class Arity a where
arityP :: Proxy a -> Int
instance {-# OVERLAPPABLE #-} Arity a where
arityP _ = 0
instance {-# OVERLAPPING #-} Arity b => Arity (a -> b) where
arityP f = 1 + arityP (Proxy :: Proxy b)
arity :: forall a. Arity a => a -> Int
arity _ = arityP (Proxy :: Proxy a)
如果您对所涉及的习惯用法感到满意,我觉得这是不言而喻的。对于您所询问的用例,当您试图在其中查找数据类型/构造函数的稀疏性时,这将非常有用。
ghci> arity T
4
如果您尝试在多态函数上使用它,那么它不起作用。
ghci> arity id
<interactive>:2:1: error:
• Overlapping instances for Arity a0 arising from a use of ‘arity’
Matching instances:
instance [overlappable] [safe] Arity a -- Defined at arity.hs:10:31
instance [overlapping] [safe] Arity b => Arity (a -> b)
-- Defined at arity.hs:13:30
这是有道理的,因为id
根据实例化的位置可能具有多个arian
id :: Int -> Int
id :: (Int -> Int) -> Int -> Int
这实际上增加了我对这种方法的信心。让我知道结果如何。