使用实例约束中的量化类型等式约束
Using a quantified type equality constraint from the instance constraints
为了设置场景,这里有一堆我们将使用的语言扩展,以及来自 CLaSH 的一些简化定义:
{-# LANGUAGE GADTs, StandaloneDeriving #-}
{-# LANGUAGE TypeOperators, DataKinds, PolyKinds #-}
{-# LANGUAGE TypeFamilyDependencies, FlexibleContexts, FlexibleInstances #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuantifiedConstraints #-}
data Signal dom a
instance Functor (Signal dom) where
instance Applicative (Signal dom) where
class Bundle a where
type Unbundled dom a = res | res -> dom a
bundle :: Unbundled dom a -> Signal dom a
unbundle :: Signal dom a -> Unbundled dom a
我想为 n 元产品类型创建 Bundle
个实例。类型本身定义如下:
import Control.Monad.Identity
data ProductF (f :: * -> *) (ts :: [*]) where
NilP :: ProductF f '[]
ConsP :: f a -> ProductF f ts -> ProductF f (a : ts)
deriving instance (Show (f t), Show (ProductF f ts)) => Show (ProductF f (t : ts))
headPF :: ProductF f (t : ts) -> f t
headPF (ConsP x xs) = x
tailP :: ProductF f (t : ts) -> ProductF f ts
tailP (ConsP x xs) = xs
-- Utilities for the simple case
type Product = ProductF Identity
infixr 5 ::>
pattern (::>) :: t -> Product ts -> Product (t : ts)
pattern x ::> xs = ConsP (Identity x) xs
headP :: Product (t : ts) -> t
headP (x ::> xs) = x
我想写的是一个 Bundle
实例,它只是将 Identity
替换为 Signal dom
。不幸的是,我们不能一次性做到这一点:
instance Bundle (Product ts) where
type Unbundled dom (Product ts) = ProductF (Signal dom) ts
bundle NilP = pure NilP
bundle (ConsP x xs) = (::>) <$> x <*> bundle xs
unbundle = _ -- Can't implement this, since it would require splitting on ts
在这里,unbundle
需要为 ts ~ []
和 ts ~ t : ts'
做一些不同的事情。好的,那么让我们尝试在两个实例中编写它:
instance Bundle (Product '[]) where
type Unbundled dom (Product '[]) = ProductF (Signal dom) '[]
bundle NilP = pure NilP
unbundle _ = NilP
instance (Bundle (Product ts), forall dom. Unbundled dom (Product ts) ~ ProductF (Signal dom) ts) => Bundle (Product (t : ts)) where
type Unbundled dom (Product (t : ts)) = ProductF (Signal dom) (t : ts)
bundle (ConsP x xs) = (::>) <$> x <*> bundle xs
unbundle xs = ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
因此,问题出现在第二个实例中。即使我们在实例约束中有一个(量化的)类型相等 forall dom. Unbundled dom (Product ts) ~ ProductF (Signal dom) ts
,GHC 8.6.3 在类型检查期间不使用它:
对于bundle
:
• Couldn't match type ‘Unbundled dom (Product ts)’
with ‘ProductF (Signal dom) ts’
Expected type: Unbundled dom (Product ts)
Actual type: ProductF (Signal dom) ts1
• In the first argument of ‘bundle’, namely ‘xs’
In the second argument of ‘(<*>)’, namely ‘bundle xs’
In the expression: (::>) <$> x <*> bundle xs
对于unbundle
:
• Couldn't match expected type ‘ProductF (Signal dom) ts’
with actual type ‘Unbundled dom (ProductF Identity ts)’
• In the second argument of ‘ConsP’, namely
‘(unbundle $ tailP <$> xs)’
In the expression: ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
In an equation for ‘unbundle’:
unbundle xs = ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
可能的解决方法
当然,我们可以选择走更远的路:专门为 Product
制作我们自己的 class,并将所有实际工作委托给它。我在这里介绍该解决方案,但我特别感兴趣的是比这更简洁和特别的东西。
class IsProduct (ts :: [*]) where
type UnbundledProd dom ts = (ts' :: [*]) | ts' -> dom ts
bundleProd :: Product (UnbundledProd dom ts) -> Signal dom (Product ts)
unbundleProd :: Signal dom (Product ts) -> Product (UnbundledProd dom ts)
instance (IsProduct ts) => Bundle (Product ts) where
type Unbundled dom (Product ts) = Product (UnbundledProd dom ts)
bundle = bundleProd
unbundle = unbundleProd
然后IsProduct
的优点是实际可以实现:
type (:::) (name :: k) (a :: k1) = (a :: k1)
instance IsProduct '[] where
type UnbundledProd dom '[] = dom ::: '[]
bundleProd NilP = pure NilP
unbundleProd _ = NilP
instance (IsProduct ts) => IsProduct (t : ts) where
type UnbundledProd dom (t : ts) = Signal dom t : UnbundledProd dom ts
bundleProd (x ::> xs) = (::>) <$> x <*> bundleProd xs
unbundleProd xs = (headP <$> xs) ::> (unbundleProd $ tailP <$> xs)
等式编码了这样一个事实,即在 '[]
和 t ': ts
两种情况下,Unbundled
系列被定义为 ProductF
。一种更简单的方法是在生成 ProductF
之前不对列表进行模式匹配。这涉及拆分 class 的 Unbundled
家族:
type family Unbundled dom a = res | res -> dom a
class Bundle a where
bundle :: Unbundled dom a -> Signal dom a
unbundle :: Signal dom a -> Unbundled dom a
因此您可以对两个 class 个实例使用一个类型实例:
type instance Unbundled dom (Product ts) = ProductF (Signal dom) ts
instance Bundle (Product '[]) where
bundle NilP = pure NilP
unbundle _ = NilP
instance (Bundle (Product ts), forall dom. Unbundled dom (Product ts) ~ ProductF (Signal dom) ts) => Bundle (Product (t : ts)) where
bundle (ConsP x xs) = (::>) <$> x <*> bundle xs
unbundle xs = ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
嗯,原则上的解决方案是单例:
-- | Reifies the length of a list
data SLength :: [a] -> Type where
SLenNil :: SLength '[]
SLenCons :: SLength xs -> SLength (x : xs)
-- | Implicitly provides @kLength@: the length of the list @xs@
class KLength xs where kLength :: SLength xs
instance KLength '[] where kLength = SLenNil
instance KLength xs => KLength (x : xs) where kLength = SLenCons kLength
单例背后的核心思想(至少其中之一)是隐式单例 class KLength
可以排除对像你这样的临时 classes 的需求. "classiness" 进入 KLength
,可以重复使用; "caseiness" 变成文字 case
,SLength
是将它们粘合在一起的数据类型。
instance KLength ts => Bundle (Product ts) where
type Unbundled dom (Product ts) = ProductF (Signal dom) ts
bundle = impl
-- the KLength xs constraint is unnecessary for bundle
-- but the recursive call would still need it, and we wouldn't have it
-- there's a rather unholy unsafeCoerce trick you can pull
-- but it's not necessary yet
where impl :: forall dom us. ProductF (Signal dom) us -> Signal dom (Product us)
impl NilP = pure NilP
impl (ConsP x xs) = (::>) <$> x <*> impl xs
unbundle = impl kLength
-- impl explicitly manages the length of the list
-- unbundle just fetches the length of ts from the givens and passes it on
where impl :: forall dom us. SLength us -> Signal dom (Product us) -> ProductF (Signal dom) us
impl SLenNil _ = NilP
impl (SLenCons n) xs = ConsP (headP <$> xs) (impl n $ tailP <$> xs)
为了设置场景,这里有一堆我们将使用的语言扩展,以及来自 CLaSH 的一些简化定义:
{-# LANGUAGE GADTs, StandaloneDeriving #-}
{-# LANGUAGE TypeOperators, DataKinds, PolyKinds #-}
{-# LANGUAGE TypeFamilyDependencies, FlexibleContexts, FlexibleInstances #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuantifiedConstraints #-}
data Signal dom a
instance Functor (Signal dom) where
instance Applicative (Signal dom) where
class Bundle a where
type Unbundled dom a = res | res -> dom a
bundle :: Unbundled dom a -> Signal dom a
unbundle :: Signal dom a -> Unbundled dom a
我想为 n 元产品类型创建 Bundle
个实例。类型本身定义如下:
import Control.Monad.Identity
data ProductF (f :: * -> *) (ts :: [*]) where
NilP :: ProductF f '[]
ConsP :: f a -> ProductF f ts -> ProductF f (a : ts)
deriving instance (Show (f t), Show (ProductF f ts)) => Show (ProductF f (t : ts))
headPF :: ProductF f (t : ts) -> f t
headPF (ConsP x xs) = x
tailP :: ProductF f (t : ts) -> ProductF f ts
tailP (ConsP x xs) = xs
-- Utilities for the simple case
type Product = ProductF Identity
infixr 5 ::>
pattern (::>) :: t -> Product ts -> Product (t : ts)
pattern x ::> xs = ConsP (Identity x) xs
headP :: Product (t : ts) -> t
headP (x ::> xs) = x
我想写的是一个 Bundle
实例,它只是将 Identity
替换为 Signal dom
。不幸的是,我们不能一次性做到这一点:
instance Bundle (Product ts) where
type Unbundled dom (Product ts) = ProductF (Signal dom) ts
bundle NilP = pure NilP
bundle (ConsP x xs) = (::>) <$> x <*> bundle xs
unbundle = _ -- Can't implement this, since it would require splitting on ts
在这里,unbundle
需要为 ts ~ []
和 ts ~ t : ts'
做一些不同的事情。好的,那么让我们尝试在两个实例中编写它:
instance Bundle (Product '[]) where
type Unbundled dom (Product '[]) = ProductF (Signal dom) '[]
bundle NilP = pure NilP
unbundle _ = NilP
instance (Bundle (Product ts), forall dom. Unbundled dom (Product ts) ~ ProductF (Signal dom) ts) => Bundle (Product (t : ts)) where
type Unbundled dom (Product (t : ts)) = ProductF (Signal dom) (t : ts)
bundle (ConsP x xs) = (::>) <$> x <*> bundle xs
unbundle xs = ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
因此,问题出现在第二个实例中。即使我们在实例约束中有一个(量化的)类型相等 forall dom. Unbundled dom (Product ts) ~ ProductF (Signal dom) ts
,GHC 8.6.3 在类型检查期间不使用它:
对于bundle
:
• Couldn't match type ‘Unbundled dom (Product ts)’ with ‘ProductF (Signal dom) ts’ Expected type: Unbundled dom (Product ts) Actual type: ProductF (Signal dom) ts1 • In the first argument of ‘bundle’, namely ‘xs’ In the second argument of ‘(<*>)’, namely ‘bundle xs’ In the expression: (::>) <$> x <*> bundle xs
对于unbundle
:
• Couldn't match expected type ‘ProductF (Signal dom) ts’ with actual type ‘Unbundled dom (ProductF Identity ts)’ • In the second argument of ‘ConsP’, namely ‘(unbundle $ tailP <$> xs)’ In the expression: ConsP (headP <$> xs) (unbundle $ tailP <$> xs) In an equation for ‘unbundle’: unbundle xs = ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
可能的解决方法
当然,我们可以选择走更远的路:专门为 Product
制作我们自己的 class,并将所有实际工作委托给它。我在这里介绍该解决方案,但我特别感兴趣的是比这更简洁和特别的东西。
class IsProduct (ts :: [*]) where
type UnbundledProd dom ts = (ts' :: [*]) | ts' -> dom ts
bundleProd :: Product (UnbundledProd dom ts) -> Signal dom (Product ts)
unbundleProd :: Signal dom (Product ts) -> Product (UnbundledProd dom ts)
instance (IsProduct ts) => Bundle (Product ts) where
type Unbundled dom (Product ts) = Product (UnbundledProd dom ts)
bundle = bundleProd
unbundle = unbundleProd
然后IsProduct
的优点是实际可以实现:
type (:::) (name :: k) (a :: k1) = (a :: k1)
instance IsProduct '[] where
type UnbundledProd dom '[] = dom ::: '[]
bundleProd NilP = pure NilP
unbundleProd _ = NilP
instance (IsProduct ts) => IsProduct (t : ts) where
type UnbundledProd dom (t : ts) = Signal dom t : UnbundledProd dom ts
bundleProd (x ::> xs) = (::>) <$> x <*> bundleProd xs
unbundleProd xs = (headP <$> xs) ::> (unbundleProd $ tailP <$> xs)
等式编码了这样一个事实,即在 '[]
和 t ': ts
两种情况下,Unbundled
系列被定义为 ProductF
。一种更简单的方法是在生成 ProductF
之前不对列表进行模式匹配。这涉及拆分 class 的 Unbundled
家族:
type family Unbundled dom a = res | res -> dom a
class Bundle a where
bundle :: Unbundled dom a -> Signal dom a
unbundle :: Signal dom a -> Unbundled dom a
因此您可以对两个 class 个实例使用一个类型实例:
type instance Unbundled dom (Product ts) = ProductF (Signal dom) ts
instance Bundle (Product '[]) where
bundle NilP = pure NilP
unbundle _ = NilP
instance (Bundle (Product ts), forall dom. Unbundled dom (Product ts) ~ ProductF (Signal dom) ts) => Bundle (Product (t : ts)) where
bundle (ConsP x xs) = (::>) <$> x <*> bundle xs
unbundle xs = ConsP (headP <$> xs) (unbundle $ tailP <$> xs)
嗯,原则上的解决方案是单例:
-- | Reifies the length of a list
data SLength :: [a] -> Type where
SLenNil :: SLength '[]
SLenCons :: SLength xs -> SLength (x : xs)
-- | Implicitly provides @kLength@: the length of the list @xs@
class KLength xs where kLength :: SLength xs
instance KLength '[] where kLength = SLenNil
instance KLength xs => KLength (x : xs) where kLength = SLenCons kLength
单例背后的核心思想(至少其中之一)是隐式单例 class KLength
可以排除对像你这样的临时 classes 的需求. "classiness" 进入 KLength
,可以重复使用; "caseiness" 变成文字 case
,SLength
是将它们粘合在一起的数据类型。
instance KLength ts => Bundle (Product ts) where
type Unbundled dom (Product ts) = ProductF (Signal dom) ts
bundle = impl
-- the KLength xs constraint is unnecessary for bundle
-- but the recursive call would still need it, and we wouldn't have it
-- there's a rather unholy unsafeCoerce trick you can pull
-- but it's not necessary yet
where impl :: forall dom us. ProductF (Signal dom) us -> Signal dom (Product us)
impl NilP = pure NilP
impl (ConsP x xs) = (::>) <$> x <*> impl xs
unbundle = impl kLength
-- impl explicitly manages the length of the list
-- unbundle just fetches the length of ts from the givens and passes it on
where impl :: forall dom us. SLength us -> Signal dom (Product us) -> ProductF (Signal dom) us
impl SLenNil _ = NilP
impl (SLenCons n) xs = ConsP (headP <$> xs) (impl n $ tailP <$> xs)