繁体   English   中英

是否有可能重新实现使用GHC泛型得出的“Enum”

[英]Is it possible to re-implement `Enum` deriving using GHC generics

是否可以使用GHC泛型重新实现Enum类型的派生?

起初,它看起来很简单:

data Foo -- representation without metadata (wrong):
  = Foo  -- L1 U1
  | Bar  -- R1 (L1 U1)
  | Baz  -- R1 (R1 (L1 U1))
  | Quux -- R1 (R1 (R1 U1))
  deriving (Show, Eq, Generic)

-- Rep Foo p = (U1 :+: (U1 :+: (U1 :+: U1))) p

instance Enum Foo where
  toEnum   = undefined -- FIXME
  fromEnum = gfromEnum . from

class GEnum f where
  gfromEnum :: f p -> Int

instance GEnum U1 where
  gfromEnum U1 = 0

instance GEnum f => GEnum (M1 i t f) where
  gfromEnum (M1 x) = gfromEnum x

instance (GEnum x, GEnum y) => GEnum (x :+: y) where
  gfromEnum (L1 x) = gfromEnum x
  gfromEnum (R1 y) = 1 + gfromEnum y

但是,这不会起作用:

λ> fromEnum Foo
0
λ> fromEnum Bar
1
λ> fromEnum Baz
1
λ> fromEnum Quux
2

这是因为我们不能依赖于(:+:)参数如何分组。 在这种情况下,似乎它们嵌套如下:

((U1 :+: U1) :+: (U1 :+: U1)) p

那么,是否可以使用Generics来派生Enum 如果有,怎么样?

GHC派生Generic ,使得L和R变体形成树,其中叶子处于Enum顺序。 请考虑以下示例(带有修剪输出):

ghci> data D = A | B | C | D | E deriving (Generic)
ghci> from A
L1 (L1 U1)
ghci> from B
L1 (R1 U1)
ghci> from C
R1 (L1 U1)
ghci> from D
R1 (R1 (L1 U1))
ghci> from E
R1 (R1 (R1 U1)))

请注意,如果您将它们排列为树, toEnum `map` [1..]叶子的左到右遍历。 有了这种直觉,我们首先定义一个GLeaves类,它计算泛型类型(不是值!)在其树中的叶子数。

{-# LANGUAGE ScopedTypeVariables, PolyKinds, TypeApplications, TypeOperators,
             DefaultSignatures, FlexibleContexts, TypeFamilies #-}

import GHC.Generics
import Data.Proxy

class GLeaves f where
  -- | Counts the number of "leaves" (i.e. U1's) in the type `f`
  gSize :: Proxy f -> Int

instance GLeaves U1 where
  gSize _ = 1

instance GLeaves x => GLeaves (M1 i t x) where
  gSize _ = gSize (Proxy :: Proxy x)

instance (GLeaves x, GLeaves y) => GLeaves (x :+: y) where
  gSize _ = gSize (Proxy :: Proxy x) + gSize (Proxy :: Proxy y)

现在,我们正在形成定义GEnum 与此设置一样,我们定义了类Enum'并具有依赖于GEnum默认签名。

class Enum' a where
  toEnum' :: Int -> a
  fromEnum' :: a -> Int

  default toEnum' :: (Generic a, GEnum (Rep a)) => Int -> a
  toEnum' = to . gToEnum

  default fromEnum' :: (Generic a, GEnum (Rep a)) => a -> Int
  fromEnum' = gFromEnum . from

class GEnum f where
  gFromEnum :: f p -> Int
  gToEnum :: Int -> f p

最后,我们得到了好的东西。 对于U1M1gFromEnumgToEnum都很简单。 对于:+: , gFromEnum需要找到它左边的所有叶子,所以如果它是正确的子树,我们添加左子树的大小(如果它是左子树,我们什么都不添加)。 类似地, gToEnum通过检查它是否小于左子树中的叶数来检查它是属于左子树还是右子树。

instance GEnum U1 where
  gFromEnum U1 = 0

  gToEnum n = if n == 0 then U1 else error "Outside enumeration range"

instance GEnum f => GEnum (M1 i t f) where
  gFromEnum (M1 x) = gFromEnum x

  gToEnum n = M1 (gToEnum n)

instance (GLeaves x, GEnum x, GEnum y) => GEnum (x :+: y) where
  gFromEnum (L1 x) = gFromEnum x
  gFromEnum (R1 y) = gSize (Proxy :: Proxy x) + gFromEnum y

  gToEnum n = let s = gSize (Proxy :: Proxy x)
              in if n < s then L1 (gToEnum n) else R1 (gToEnum (n - s))

最后,您可以在GHCi中测试:

ghci> :set -XDeriveAnyClass -XDeriveGeneric
ghci> data D = A | B | C | D | E deriving (Show, Generic, Enum, Enum')
ghci> toEnum `map` [0 .. 4] :: [D]
[A,B,C,D,E]
ghci> toEnum' `map` [0 .. 4] :: [D]
[A,B,C,D,E]
ghci> fromEnum `map` [A .. E] :: [Int]
[A,B,C,D,E]
ghci> fromEnum' `map` [A .. E] :: [Int]
[A,B,C,D,E]

性能

你可能会想到自己:这是超级低效的! 我们最终反复重新计算一堆大小 - 最糟糕的情况是性能至少为O(n^2) 问题是(希望如此),GHC将能够优化/内联我们特定的Enum'实例,直到最初的Generic结构没有剩余

Enum是使用标准GHC Generics表示稍微难以编写的许多示例之一,因为数据类型的许多结构都是隐式的(例如,sum和product构造函数是如何嵌套的,以及元数据发生的位置)。

使用generics-sop ,您可以(重新)以更直接的方式定义通用Enum实例:

{-# LANGUAGE ConstraintKinds, DataKinds, DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts, GADTs, PolyKinds #-}
{-# LANGUAGE TypeApplications, TypeOperators #-}

import Generics.SOP
import qualified GHC.Generics as GHC

我们定义了一个类型同义词,它捕获了枚举类型的含义:

type IsEnumType a = All ((~) '[]) (Code a)

(不幸的是, (~) '[]构造触发了GHC 8.0.1中的错误,但它在GHC 8.0.2中工作正常。)在泛型中,数据类型的代码是列表的类型级列表。 外部列表包含每个构造函数的元素,内部列表包含构造函数参数的类型。 IsEnumType约束表示所有内部列表必须为空,这意味着没有任何构造函数必须具有任何参数。

gfromEnum :: (Generic a, IsEnumType a) => a -> Int
gfromEnum = conIndex . unSOP . from
  where
    conIndex :: NS f xs -> Int
    conIndex (Z _) = 0
    conIndex (S i) = 1 + conIndex i

该函数from变为一个值加总产品的表示,和unSOP剥去外构造。 然后我们有一个求和结构来遍历。 表示n元和的NS数据类型具有构造函数ZS ,它们准确地指示了正在使用的构造函数,因此我们可以简单地遍历和计数。

gtoEnum :: (Generic a, IsEnumType a) => Int -> a
gtoEnum i =
  to (SOP (apInjs_NP (hcpure (Proxy @ ((~) '[])) Nil) !! i))

这里, apInjs_NP (hcpure ...)调用为数据类型的所有构造函数生成空构造函数应用程序的表示。 gfromEnum函数不同,这实际上使用IsEnumType约束是正确的类型(因为我们依赖于没有任何构造函数接受任何参数的事实)。 然后,我们选择了i个构造出来的名单,并通过应用第一再将其从通用表示实际类型SOP ,然后to

要将它应用于您的样本类型,您必须将它实例化为GHC和generic-sop的Generic类(或者您也可以使用TH):

data Foo = Foo | Bar | Baz | Quux
  deriving (Show, Eq, GHC.Generic)

instance Generic Foo

然后你可以测试它:

GHCi> gfromEnum Baz
2
GHCi> gtoEnum 2 :: Foo
Baz

如果需要,可以将gfromEnumgtoEnum设置为类似Enum的类的默认定义,就像使用GHC Generics一样。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM