我想计算某类数据类型的“ arity” 。即具有单个构造函数和一定数量字段的数据类型。例如data T a = T Int () String a。然后,“ arity”将是字段数。对于T a这将是4。我设想一个带有如下签名的函数:
forall a . C a => Int
Run Code Online (Sandbox Code Playgroud)
对于一些适当的选择C。我知道如果我有Generic a某种类型的话,a我会得到from :: a -> Rep a x,但是请注意,这将需要一个具体的值a,我对静态地进行计算很感兴趣。这有可能吗?我也考虑过了Typeable,但是我不太了解API。
我们可以使用泛型。在整个答案中使用了很多扩展,这些扩展对于各种元编程都很常见。我将第一次使用它们,但有关更多详细信息,请参阅其他资源,例如GHC用户指南(扩展列表)或Haskell Wiki。
data T = T Int Bool String deriving Generic
-- Used extension: DeriveGeneric
Run Code Online (Sandbox Code Playgroud)
派生实例包括类型家庭实例Rep,用于构造类型的通用表示形式T。Rep T使用一组固定的中发现的多种类型的GHC.Generics模块:
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
Run Code Online (Sandbox Code Playgroud)
我们将定义一个类型级别的函数来检查该结构并计算字段数。这是它的签名:
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
Run Code Online (Sandbox Code Playgroud)
当涉及泛型表示时,我们可以假设它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
Run Code Online (Sandbox Code Playgroud)
非常(太?)简短的概述。M1包含诸如类型名称(包括模块和包),构造函数名称,构造函数是否使用记录符号,字段严格性等信息,V1并且(:+:)用于零个或多个构造函数的类型,因此它们与我们无关。U1代表null构造函数,而(:*:)拆分n进制构造函数,在任一侧代表一半字段。K1标记一个构造函数字段。
我们Arity通过为函数指定类型实例来定义函数。但是,实际上,对于最初的理解,忽略type instance关键字,并假装Arity是通常通过模式匹配定义的功能。
查看Rep T上面的表示,我们首先遇到一个M1节点,我们将忽略它并递归调用Arity它的内容。
type instance Arity (M1 i c f) = Arity f
Run Code Online (Sandbox Code Playgroud)
然后,我们(:*:)将把一组字段分为两部分。我们以递归方式计算它们的arities并将它们加起来。
type instance Arity (f :*: g) = Arity f + Arity g
-- Used extensions: TypeOperators, UndecidableInstances
Run Code Online (Sandbox Code Playgroud)
U1 代表无效构造函数,
type instance Arity U1 = 0
Run Code Online (Sandbox Code Playgroud)
并且K1是单个字段。
type instance (K1 i a) = 1
Run Code Online (Sandbox Code Playgroud)
现在,给定通用类型T(即带有的实例Generic)的Arity (Rep T)是它的Arity,作为类型级别Nat。在ghci中,我们可以使用
:kind! Arity (Rep T)
Run Code Online (Sandbox Code Playgroud)
使用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
Run Code Online (Sandbox Code Playgroud)
我们将任何泛型类型的arity T用作value arity @T,可以使用fromIntegral :: Natural -> Integer例如进行转换。
main = print (arity @T)
Run Code Online (Sandbox Code Playgroud)
完整要点:https : //gist.github.com/Lysxia/10f1da354f051b2d2eb24f6aace1bf9c
为了在评论中回答我的问题,下面是一个示例,说明如何查找函数的稀疏性。
{-# 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)
Run Code Online (Sandbox Code Playgroud)
如果您对所涉及的习语感到满意,我觉得这是不言而喻的。对于您要查询的用例,在试图找到数据类型/构造函数的泛用的情况下,这将很好地工作。
ghci> arity T
4
Run Code Online (Sandbox Code Playgroud)
如果您尝试在多态函数上使用它,那么它不起作用。
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
Run Code Online (Sandbox Code Playgroud)
这是有道理的,因为id根据实例化的位置,它可能具有多个arity
id :: Int -> Int
id :: (Int -> Int) -> Int -> Int
Run Code Online (Sandbox Code Playgroud)
这实际上增加了我对这种方法的信心。让我知道它是如何工作的。