如何“显示”不可显示的类型?

问题描述

我正在使用 data-reifygraphviz 将 eDSL 转换为漂亮的图形表示,用于内省目的。

作为简单的人为例子,考虑:

{-# LANGUAGE GADTs #-}

data Expr a where
  Constant :: a -> Expr a
  Map :: (other -> a) -> Expr a -> Expr a
  Apply :: Expr (other -> a) -> Expr a -> Expr a

instance Functor Expr where
  fmap fun val = Map fun val

instance applicative Expr where
  fun_expr <*> data_expr = Apply fun_expr data_expr
  pure val = Constant val

-- And then some functions to optimize an Expr AST,evaluate Exprs,etc.

为了使内省更好,我想打印存储在 DSL 数据类型的某些 AST 节点内的值。 但是,通常任何 a 都可能存储在 Constant 中,即使那些没有实现 Show。这不一定是问题,因为我们可以像这样约束 Expr 的实例:

instance Show a => Show (Expr a) where
  ...

然而,这不是我想要的:即使 Expr 不支持 a,我仍然希望能够打印 Show,通过打印一些占位符值(例如只是它的类型和它不可打印的消息)。

因此,如果我们有一个 a 实现了 Show,我们希望做一件事,如果特定的 a 没有实现另一件事。

此外,DSL 还具有构造函数 MapApply,它们的问题更大。构造函数other 中是存在的,因此我们不能假设任何关于 othera(other -> a)。将 other 类型的约束添加Map 响应。 Apply 构造函数会破坏 Functor 的实现。 applicative 转发给他们。

在这里我也想打印功能

  • 唯一的参考。使用 unsafePerformIO 这总是可能的(即使它不漂亮,因为它需要 System.Mem.StableName)。
  • 如果可能,它的类型(一种技术是使用 show (typeOf fun),但它要求 funTypeable)。

我们再次遇到一个问题,如果我们有一个 f 实现了 Typeable,我们想做一件事,如果 f 没有实现,我们想做另一件事。

如何做到这一点?


额外免责声明:此处的目标不是为不支持它的类型创建“正确的”Show 实例。不希望以后能够Read,或者print a != print b暗示a != b

目标是以“适合人类内省”的方式打印任何数据结构。

我坚持的部分是,如果额外的约束对 a 分别持有,我想使用一种实现。 (other -> a),但如果这些不存在,则为“认”。 也许带有 FlexibleInstances 的类型类,或者这里可能需要类型系列?我一直无法弄清楚(也许我一直在错误的轨道上)。

解决方法

并非所有问题都有解决方案。并非所有约束系统都有令人满意的分配。

所以……放宽限制。将在数据结构中创建合理的内省函数所需的数据存储起来,并使用带有类型签名的函数like showfmappure 和 { {1}},但不完全等于它们。如果需要 (<*>),请在类型签名中使用 IO。简而言之:将您的特殊需求纳入标准库的期望中解放出来。

要处理您可能有实例或没有实例的事情,请存储说明您是否有实例的数据:

IO

(也许 data InstanceOrNot c where Instance :: c => InstanceOrNot c Not :: InstanceOrNot c -kinded Constraint-alike,而不是 Either-alike,会更合适。我怀疑当您开始编码时,您会发现需要什么。)要求致电 Maybe 的客户和朋友酌情提供这些信息。

在评论中,我建议通过您需要的约束来参数化您的类型,并为无约束版本提供一个 notFmap 实例。这是一个简短的示例,展示了它的外观:

Functor
,

好时机! Well-typed 最近发布了一个库,它允许您恢复运行时信息。他们特别有一个显示任意值的例子。它位于 github 上的 https://github.com/well-typed/recover-rtti

,

事实证明,这是一个过去被多人认识到的问题,称为“受约束的 Monad 问题”。有一个优雅的解决方案,在 Neil Sculthorpe 和 Jan Bracker 以及 George Giorgidze 和 Andy Gill 的论文 The Constrained-Monad Problem 中有详细解释。

技术的简要总结:Monads(和其他类型类)有一个“正常形式”。我们可以将原语(以我们希望的任何方式受到约束)“提升”到这个“正常形式”结构中,它本身就是一种存在数据类型,然后使用我们提升到的类型类可用的任何操作。这些操作本身不受约束,因此我们可以使用 Haskell 的所有普通类型类函数。 最后,为了将其转回具体类型(它再次具有我们感兴趣的所有约束),我们“降低”它,这是一个操作,它为每个类型类的操作采用一个函数,它将在适当的时候应用时间。 这样,来自外部的约束(这是提供给降低的功能的一部分)和来自内部的约束(这是我们提升的原语的一部分)能够匹配,最后我们得到一个大的快乐约束我们已经能够使用任何普通的 Functor/Monoid/Monad/等的数据类型。操作。

有趣的是,虽然中间操作不受约束,但据我所知,不可能编写一些“破坏”它们的东西,因为这会破坏所考虑的类型类应遵守的分类法则。

这在 constrained-normal Hackage 包中可用,可在您自己的代码中使用。

我挣扎的例子,可以实现如下:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE UndecidableInstances #-}

module Example where

import Data.Dynamic
import Data.Kind
import Data.Typeable

import Control.Monad.ConstrainedNormal


-- | Required to have a simple constraint which we can use as argument to `Expr` / `Expr'`.
-- | This is definitely the part of the example with the roughest edges: I have yet to figure out
-- | how to make Haskell happy with constraints 
class (Show a,Typeable a) => Introspectable a where {}
instance (Show a,Typeable a) => Introspectable a where {}

data Expr' (c :: * -> Constraint) a where
  C :: a -> Expr' c a
  -- M :: (a -> b) -> Expr' a -> Expr' b --^ NOTE: This one is actually never used as ConstrainedNormal will use the 'free' implementation based on A + C.
  A :: c a => Expr' c (a -> b) -> Expr' c a -> Expr' c b


instance Introspectable a => Show (Expr' Introspectable a) where
  show e = case e of
    C x -> "(C " ++ show x ++ ")"
    -- M f x = "(M " ++ show val ++ ")"
    A fx x -> "(A " ++ show (typeOf fx) ++ " " ++ show x ++ ")"

-- | In user-facing code you'd not want to expose the guts of this construction
--   So let's introduce a 'wrapper type' which is what a user would normally interact with.
type Expr c a = NAF c (Expr' c) a

liftExpr :: c a => Expr' c a -> Expr c a
liftExpr expr = liftNAF expr

lowerExpr :: c a => Expr c a -> Expr' c a
lowerExpr lifted_expr = lowerNAF C A lifted_expr

constant :: Introspectable a => a -> Expr c a
constant val = pure val -- liftExpr (C val)

你现在可以写

ghci> val = constant 10 :: Expr Introspectable Int
(C 10)
ghci> (+2) <$> val
(C 12)
ghci> (+) <$> constant 10 <*> constant 32  :: Expr Introspectable Int

并且通过使用 Data.Constraint.Trivialtrivial-constrained 库的一部分,尽管也可以编写自己的“空约束”),可以改为编写例如

ghci> val = constant 10 :: Expr Unconstrained Int

这会像以前一样工作,但现在无法打印 val


我还没有想出的一件事是如何正确处理约束的子集(即,如果我有一个只需要 Show 的函数,让它与 Introspectable )。目前,一切都必须与“大”约束集一起使用。 另一个小缺点当然是您必须注释约束类型(例如,如果您不需要约束,请手动编写 Unconstrained),否则 GHC 会抱怨 c0 未知。


我们已经达到了拥有一种可以可选被限制为可打印的类型的目标,所有不需要打印的机器也可以在该类型系列的所有实例上工作,包括那些不可打印的,并且类型可以根据需要用作 Monoids、Functors、Applicatives 等。

我认为这是一个很好的方法,并想赞扬 Neil Sculthorpe 等人。感谢他们在论文和 constrained-normal 库上所做的工作,使这成为可能。很酷!