【问题标题】:Constrained closed type family约束封闭型族
【发布时间】:2015-06-24 05:33:34
【问题描述】:

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

类似的东西

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 

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

【问题讨论】:

  • 我也想要这个,但不幸的是,据我所知这是不可能的。尽管我认为使用单例,但您只需要 1 个类。
  • 为什么不只是parseF :: forall lt. (Read (Synonym lt), Show (Synonym lt)) => SLiftedType lt -> String -> String?据我了解,它足以满足您的目的。
  • @AndrásKovács 我为我的激励示例添加了一些进一步的上下文。 SLiftedType lt 的值事先不知道 - 我试图将 (String, String) 解析为 (LiftedType, String),然后解析为 (SLiftedType lt, Synonym lt),但将依赖类型的部分隐藏在 SomeSing case 语句中。
  • @bennofs - 只需要一节课是什么意思?
  • @dbeacham 我不认为这会排除我的提议。只需对SomeSing slt 中的slt 进行模式匹配,您就可以在那里处理不可显示/不可读取的情况。

标签: haskell type-families


【解决方案1】:

问题是我们不能将Synonym 放在实例头部,因为它是一个类型族,而且我们不能写像(forall x. Show (Synonym x)) => ... 这样的“普遍量化”约束,因为在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 

这并没有直接为我们提供Read (Synonym t) 用于t 的任何特定选择,尽管我们仍然可以读取Some SynonymS,然后在存在标签上进行模式匹配以检查我们是否获得了正确的类型(如果失败则失败)这是不对的)。这几乎涵盖了read 的所有用例。

如果这还不够好,我们可以使用另一个包装器,并将 Some f 实例提升为“普遍量化”的实例。

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

【讨论】:

  • 我假设在答案中非常熟悉singletons。请询问是否有不清楚的地方。
【解决方案2】:

如果我正确理解您想要做什么,这是不可能的。如果是的话,你可以很容易地构造一个Proxy t -&gt; 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 的一个特性或限制,取决于你的观点)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-02
    • 1970-01-01
    • 1970-01-01
    • 2016-10-28
    • 2019-01-14
    • 1970-01-01
    • 2019-03-31
    • 2023-04-10
    相关资源
    最近更新 更多