问题描述
我正在使用 data-reify
和 graphviz
将 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 还具有构造函数 Map
和 Apply
,它们的问题更大。构造函数在 other
中是存在的,因此我们不能假设任何关于 other
、a
或 (other -> a)
。将 other
类型的约束添加到 Map
响应。 Apply
构造函数会破坏 Functor
的实现。 applicative
转发给他们。
- 唯一的参考。使用
unsafePerformIO
这总是可能的(即使它不漂亮,因为它需要System.Mem.StableName
)。 - 如果可能,它的类型(一种技术是使用
show (typeOf fun)
,但它要求fun
为Typeable
)。
我们再次遇到一个问题,如果我们有一个 f
实现了 Typeable
,我们想做一件事,如果 f
没有实现,我们想做另一件事。
如何做到这一点?
额外免责声明:此处的目标不是为不支持它的类型创建“正确的”Show
实例。不希望以后能够Read
,或者print a != print b
暗示a != b
。
目标是以“适合人类内省”的方式打印任何数据结构。
我坚持的部分是,如果额外的约束对 a
分别持有,我想使用一种实现。 (other -> a)
,但如果这些不存在,则为“默认”。
也许带有 FlexibleInstances
的类型类,或者这里可能需要类型系列?我一直无法弄清楚(也许我一直在错误的轨道上)。
解决方法
并非所有问题都有解决方案。并非所有约束系统都有令人满意的分配。
所以……放宽限制。将在数据结构中创建合理的内省函数所需的数据存储起来,并使用带有类型签名的函数like show
、fmap
、pure
和 { {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.Trivial
(trivial-constrained 库的一部分,尽管也可以编写自己的“空约束”),可以改为编写例如
ghci> val = constant 10 :: Expr Unconstrained Int
这会像以前一样工作,但现在无法打印 val
。
我还没有想出的一件事是如何正确处理约束的子集(即,如果我有一个只需要 Show
的函数,让它与 Introspectable
)。目前,一切都必须与“大”约束集一起使用。
另一个小缺点当然是您必须注释约束类型(例如,如果您不需要约束,请手动编写 Unconstrained
),否则 GHC 会抱怨 c0
未知。
我们已经达到了拥有一种可以可选被限制为可打印的类型的目标,所有不需要打印的机器也可以在该类型系列的所有实例上工作,包括那些不可打印的,并且类型可以根据需要用作 Monoid
s、Functor
s、Applicative
s 等。
我认为这是一个很好的方法,并想赞扬 Neil Sculthorpe 等人。感谢他们在论文和 constrained-normal
库上所做的工作,使这成为可能。很酷!