是否可以使用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
那么,是否可以使用Enum
派生Generics
?如果是,怎么样?
答案 0 :(得分:4)
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
最后,我们得到了好东西。对于U1
和M1
,gFromEnum
和gToEnum
都很简单。对于:+:
,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
结构没有剩余。
答案 1 :(得分:2)
Enum是使用标准GHC Generics表示编写稍微笨拙的众多示例之一,因为很多数据类型的结构都是隐式的(例如,sum和product构造函数是如何嵌套的,以及元数据发生的位置)。 / p>
使用generics-sop,您可以(重新)以更直接的方式定义通用枚举实例:
{-# 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中工作正常。)在generics-sop中,数据类型的代码是列表的类型级列表。外部列表包含每个构造函数的元素,内部列表包含构造函数参数的类型。 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
数据类型具有构造函数Z
和S
,它们准确地指示了正在使用的构造函数,因此我们可以简单地遍历和计数。
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
类(或者您也可以使用TH):< / p>
data Foo = Foo | Bar | Baz | Quux
deriving (Show, Eq, GHC.Generic)
instance Generic Foo
然后你可以测试它:
GHCi> gfromEnum Baz
2
GHCi> gtoEnum 2 :: Foo
Baz
如果需要,您可以为类似Enum的类提供gfromEnum
和gtoEnum
默认定义,就像使用GHC Generics一样。