通常确定数据类型的一致性

时间:2019-05-28 20:26:02

标签: haskell

我想计算特定类别的数据类型的“ 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。

2 个答案:

答案 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

Arity函数

我们将定义一个类型级别的函数来检查该结构并计算字段数。这是它的签名:

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

这实际上增加了我对这种方法的信心。让我知道结果如何。