haskell - Haskell 不使用类型类的更具体的实例
问题描述
在过去的几天里,我一直在弄清楚我正在尝试做的事情在 Haskell 中是否真的可行。
这是一些上下文:我正在尝试编写一种小标记语言(类似于 ReST),其中语法已经通过指令启用了自定义扩展。对于实现新指令的用户,他们应该能够在文档数据类型中添加新的语义结构。例如,如果想要添加一个用于显示数学的指令,他们可能希望MathBlock String
在 ast 中有一个构造函数。
DirectiveBlock String
显然数据类型是不可扩展的,并且有一个包含指令名称的通用构造函数的解决方案(这里"math"
,论据)。
使用类型族,我制作了类似的原型:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
-- Arguments for custom directives.
data family Args :: * -> *
data DocumentBlock
= Paragraph String
| forall a. Block (Args a)
果然,如果有人想为数学显示定义一个新的指令,他们可以这样做:
data Math
-- The expected arguments for the math directive.
data instance Args Math = MathArgs String
doc :: [DocumentBlock]
doc =
[ Paragraph "some text"
, Block (MathArgs "x_{n+1} = x_{n} + 3")
]
到目前为止一切顺利,我们只能构建指令块接收正确参数的文档。
当一个用户想要将文档的内部表示转换为一些自定义输出(例如,字符串)时,就会出现问题。用户需要为所有指令提供默认输出,因为会有很多指令,其中一些无法转换为目标。此外,用户可能希望为某些指令提供更具体的输出:
class StringWriter a where
write :: Args a -> String
-- User defined generic conversion for all directives.
instance StringWriter a where
write _ = "Directive"
-- Custom way of showing the math directive.
instance StringWriter Math where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- Then to display a DocumentBlock
writeBlock :: DocumentBlock -> String
writeBlock (Paragraph t) = "Paragraph(" ++ t ++ ")"
writeBlock (Block args) = write args
main :: IO ()
main = putStrLn $ writeBlock (Block (MathArgs "a + b"))
在此示例中,输出 isBlock
和 not Math(a+b)
,因此始终选择 StringWriter 的通用实例。即使玩{-# OVERLAPPABLE #-}
,也没有成功。
我在 Haskell 中描述的那种行为是否可能?
当尝试在Block
定义中包含通用 Writer 时,它也无法编译。
-- ...
class Writer a o where
write :: Args a -> o
data DocumentBlock
= Paragraph String
| forall a o. Writer a o => Block (Args a)
instance {-# OVERLAPPABLE #-} Writer a String where
write _ = "Directive"
instance {-# OVERLAPS #-} Writer Math String where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- ...
解决方案
您的代码无法编译,因为Block something
有 type DocumentBlock
,而write
需要一个Args a
参数,并且这两种类型是不同的。你的意思是writeBlock
相反吗?我会假设是这样。
您可能想要尝试的是在您的存在类型中添加一个约束,例如:
data DocumentBlock
= Paragraph String
| forall a. StringWriter a => Block (Args a)
-- ^^^^^^^^^^^^^^ --
这具有以下效果。在操作上,每次Block something
使用时,都会记住实例(指针隐含地存储在Args a
值中)。这将是一个指向包罗万象的实例或特定实例的指针,以最合适的为准。
当构造函数稍后进行模式匹配时,可以使用该实例。完整的工作代码:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
-- Arguments for custom directives.
data family Args :: * -> *
data DocumentBlock
= Paragraph String
| forall a. StringWriter a => Block (Args a)
data Math
-- The expected arguments for the math directive.
data instance Args Math = MathArgs String
doc :: [DocumentBlock]
doc =
[ Paragraph "some text"
, Block (MathArgs "x_{n+1} = x_{n} + 3")
]
class StringWriter a where
write :: Args a -> String
-- User defined generic conversion for all directives.
instance {-# OVERLAPPABLE #-} StringWriter a where
write _ = "Directive"
-- Custom way of showing the math directive.
instance StringWriter Math where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- Then to display a DocumentBlock
writeBlock :: DocumentBlock -> String
writeBlock (Paragraph t) = "Paragraph(" ++ t ++ ")"
writeBlock (Block args) = write args
main :: IO ()
main = putStrLn $ writeBlock (Block (MathArgs "a + b"))
这打印Math(a + b)
。
最后一点:要使其工作,所有相关实例在Block
使用时都在范围内是至关重要的。否则,GHC 可能会选择错误的实例,导致一些意外的输出。这是主要限制,使重叠实例通常有点脆弱。只要没有孤立实例,这应该可以工作。
另请注意,如果使用其他存在类型,用户可能(有意或无意地)导致 GHC 选择错误的实例。例如,如果我们使用
data SomeArgs = forall a. SomeArgs (Args a)
toGenericInstance :: DocumentBlock -> DocumentBlock
toGenericInstance (Block a) = case SomeArgs a of
SomeArgs a' -> Block a' -- this will always pick the generic instance
toGenericInstance db = db
然后,writeBlock (toGenericInstance (Block (MathArgs "a + b")))
将产生Directive
代替。
推荐阅读
- cobol - 当我想从 cobol 程序显示 ispf 面板时出现错误 rc=20
- ffmpeg - 错误:不可满足的约束:so:libvpx.so.6(缺失)
- python - TypeError:只能将元组(不是“str”)连接到元组 TROUBLES
- asynchronous - 无法从 AWS lambda 调用 AWS Cognito API,但相同的代码在本地 node.js 中运行良好
- vue.js - 如何使用 vue/vuex 从输入中过滤数据?
- python - 基本循环算法的时间复杂度
- mongodb - Mongo 的聚合帮助 - 为什么硬编码有效而不是 req.body?
- airflow - 如何防止气流中的“执行失败:[Errno 32] Broken pipe”
- android - 在连接的设备上运行颤振应用程序时出现问题
- shopify - 调用 page_title 到 Shopify 页面内容