在过去的几天里,我一直在弄清楚我正在尝试做的事情在 Haskell 中是否真的可行。
这是一些上下文:
我正在尝试编写一种小标记语言(类似于 ReST),其中语法已经通过指令启用了自定义扩展。
对于实现新指令的用户,他们应该能够在文档数据类型中添加新的语义结构。例如,如果想要添加一个显示数学的指令,他们可能想要一个 MathBlock String
ast 中的构造函数。
显然数据类型是不可扩展的,有泛型构造函数的解决方案DirectiveBlock String
包含指令的名称(此处为 "math"
)是不可取的,因为我们希望在我们的 ast 中只有格式正确的构造(因此只有带有格式正确参数的指令)。
使用类型族,我制作了类似的原型(prototype):
{-# 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")
]
到目前为止一切顺利,我们只能构建指令 block 接收正确参数的文档。
当一个用户想要将文档的内部表示转换为一些自定义输出(例如,字符串)时,就会出现问题。
用户需提供所有指令的默认输出 ,因为会有很多,其中一些无法转换为目标。
此外,用户可能希望提供 一些指令的更具体的输出 :
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"))
在此示例中,输出为
Block
而不是 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
有类型 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
反而。
关于Haskell 不使用类型类的更具体的实例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54039876/