约束封闭型族

Constrained closed type family

我能否使编译器相信封闭类型族中的类型同义词始终满足约束条件?该系列由一组有限的提升值索引。

类似于

data NoShow = NoShow
data LiftedType = V1 | V2 | V3

type family (Show (Synonym (a :: LiftedType)) => Synonym (a :: LiftedType)) where
  Synonym V1 = Int
  Synonym V2 = NoShow -- no Show instance => compilation error
  Synonym V3 = ()

我可以对开放类型族实施约束:

class (Show (Synonym a)) => SynonymClass (a :: LiftedType) where
  type Synonym a
  type Synonym a = ()

instance SynonymClass Int where
  type Synonym V1 = Int

-- the compiler complains here
instance SynonymClass V2 where
  type Synonym V2 = NoShow

instance SynonymClass V3

但是编译器必须能够推断出 V1V2V3 中的每一个都存在一个 SynonymClass a 的实例这一事实?但无论如何,我宁愿不使用开放类型的家庭。

我要求这样做的动机是我想说服编译器我的代码中封闭类型系列的所有实例都有 Show/Read 个实例。一个简化的例子是:

parseLTandSynonym :: LiftedType -> String -> String
parseLTandSynonym lt x =
  case (toSing lt) of
    SomeSing (slt :: SLiftedType lt') -> parseSynonym slt x

parseSynonym :: forall lt. SLiftedType lt -> String -> String
parseSynonym slt flv =
      case (readEither flv :: Either String (Synonym lt)) of
        Left err -> "Can't parse synonym: " ++ err
        Right x  -> "Synonym value: " ++ show x 

[有人在评论中提到这是不可能的 - 这是因为它在技术上是不可能的(如果是的话,为什么)或者只是 GHC 实施的限制?]

问题是我们不能把Synonym放在一个实例头中,因为它是一个类型族,我们不能像(forall x. Show (Synonym x)) => ...那样写一个"universally quantified"约束,因为有Haskell 中没有这样的东西。

但是,我们可以使用两个东西:

  • forall x. f x -> a 等价于 (exists x. f x) -> a
  • singletons 的去功能化让我们无论如何都可以将类型族放入实例头中。

因此,我们定义了一个适用于 singletons 样式类型函数的存在包装器:

data Some :: (TyFun k * -> *) -> * where
  Some :: Sing x -> f @@ x -> Some f

而且我们还包括 LiftedType 的去功能化符号:

import Data.Singletons.TH
import Text.Read
import Control.Applicative

$(singletons [d| data LiftedType = V1 | V2 | V3 deriving (Eq, Show) |])

type family Synonym t where
  Synonym V1 = Int
  Synonym V2 = ()
  Synonym V3 = Char

data SynonymS :: TyFun LiftedType * -> * -- the symbol for Synonym
type instance Apply SynonymS t = Synonym t

现在,我们可以用Some SynonymS -> a代替forall x. Synonym x -> a,这种形式也可以在实例中使用。

instance Show (Some SynonymS) where
  show (Some SV1 x) = show x
  show (Some SV2 x) = show x
  show (Some SV3 x) = show x

instance Read (Some SynonymS) where
  readPrec = undefined -- I don't bother with this now...

parseLTandSynonym :: LiftedType -> String -> String
parseLTandSynonym lt x =
  case (toSing lt) of
    SomeSing (slt :: SLiftedType lt') -> parseSynonym slt x

parseSynonym :: forall lt. SLiftedType lt -> String -> String
parseSynonym slt flv =
      case (readEither flv :: Either String (Some SynonymS)) of
        Left err -> "Can't parse synonym: " ++ err
        Right x  -> "Synonym value: " ++ show x 

虽然我们仍然可以读取 Some SynonymS 然后在存在标记上进行模式匹配以检查我们是否得到正确的类型(如果不正确则失败)。这几乎涵盖了 read.

的所有用例

如果这还不够好,我们可以使用另一个包装器,并将 Some f 个实例提升到 "universally quantified" 个实例。

data At :: (TyFun k * -> *) -> k -> * where
  At :: Sing x -> f @@ x -> At f x

At f x等同于f @@ x,但是我们可以为它写实例。特别是,我们可以在这里编写一个合理的通用 Read 实例。

instance (Read (Some f), SDecide (KindOf x), SingKind (KindOf x), SingI x) =>
  Read (At f x) where
    readPrec = do
      Some tag x <- readPrec :: ReadPrec (Some f)
      case tag %~ (sing :: Sing x) of
        Proved Refl -> pure (At tag x)
        Disproved _ -> empty

我们首先解析一个Some f,然后检查解析的类型索引是否等于我们要解析的索引。它是我上面提到的用于解析具有特定索引的类型的模式的抽象。它更方便,因为我们在 At 上的模式匹配中只有一个案例,无论我们有多少索引。请注意 SDecide 约束。它提供了 %~ 方法,如果我们在单例数据定义中包含 deriving Eqsingletons 会为我们派生它。投入使用:

parseSynonym :: forall lt. SLiftedType lt -> String -> String
parseSynonym slt flv = withSingI slt $ 
      case (readEither flv :: Either String (At SynonymS lt)) of
        Left err -> "Can't parse synonym: " ++ err
        Right (At tag x)  -> "Synonym value: " ++ show (Some tag x :: Some SynonymS)

我们还可以使 AtSome 之间的转换更容易一些:

curry' :: (forall x. At f x -> a) -> Some f -> a
curry' f (Some tag x) = f (At tag x)

uncurry' :: (Some f -> a) -> At f x -> a
uncurry' f (At tag x) = f (Some tag x)

parseSynonym :: forall lt. SLiftedType lt -> String -> String
parseSynonym slt flv = withSingI slt $ 
      case (readEither flv :: Either String (At SynonymS lt)) of
        Left err -> "Can't parse synonym: " ++ err
        Right atx  -> "Synonym value: " ++ uncurry' show atx

如果我理解正确的话,这是不可能的。如果是的话,你可以很容易地构造一个Proxy t -> Bool类型的非常量函数,沿着

data YesNo = Yes | No
class Foo (yn :: YesNo) where foo :: Proxy yn -> Bool
type family (Foo (T t) => T t) where
    T X = Yes
    T y = No

f :: forall t. Proxy t -> Bool
f _ = foo (Proxy (T t))

但是你不能构造这样的函数,即使涉及到的所有类型都是封闭的(这当然是 GHC 的一个特性或限制,取决于你的观点)。