Haskell不使用更具体的typeclass实例
在过去的几天里,我一直很难弄清楚我正在尝试做的事情在哈斯克尔是否切实可行 以下是一些背景: 我正在尝试编写一种小标记语言(类似于ReST),其中的语法已经通过指令启用了自定义扩展。 对于要实现新指令的用户,他们应该能够在文档数据类型中添加新的语义结构。例如,如果想要添加一个用于显示数学的指令,他们可能希望ast中有一个Haskell不使用更具体的typeclass实例,haskell,instance,typeclass,extensibility,Haskell,Instance,Typeclass,Extensibility,在过去的几天里,我一直很难弄清楚我正在尝试做的事情在哈斯克尔是否切实可行 以下是一些背景: 我正在尝试编写一种小标记语言(类似于ReST),其中的语法已经通过指令启用了自定义扩展。 对于要实现新指令的用户,他们应该能够在文档数据类型中添加新的语义结构。例如,如果想要添加一个用于显示数学的指令,他们可能希望ast中有一个MathBlock字符串构造函数 很明显,数据类型是不可扩展的,并且存在一个通用构造函数DirectiveBlock String包含指令名称(此处为“math”)的解决方案是不可
MathBlock字符串
构造函数
很明显,数据类型是不可扩展的,并且存在一个通用构造函数DirectiveBlock String
包含指令名称(此处为“math”
)的解决方案是不可取的,因为我们希望在ast中只包含格式良好的构造(因此只包含格式良好的参数的指令)
使用类型族,我创建了如下原型:
{-# 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)
果不其然,如果有人希望为math display定义新指令,他们可以这样做:
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"))
在本例中,输出是Block
,而不是Math(a+b)
,因此始终选择StringWriter的通用实例。即使在玩{-#可重叠#-}
时,也不会成功
我所描述的这种行为在Haskell中可能吗
当试图在
块
定义中包含泛型编写器时,它也无法编译
-- ...
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
需要一个参数,并且两种类型不同。
你的意思是说writeBlock
?我想是的
您可能希望尝试在存在类型中添加约束,例如:
data DocumentBlock
= Paragraph String
| forall a. StringWriter a => Block (Args a)
-- ^^^^^^^^^^^^^^ --
这具有以下效果。在操作上,每次使用块某物
,实例都会被记住(一个指针隐式地存储在Args a
值上)。这将是指向catch all实例或特定实例的指针,以最合适的为准
当随后对构造函数进行模式匹配时,就可以使用实例了。完整工作代码:
{-# 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"))
这将打印数学(a+b)
最后一点注意:要使其起作用,当使用块时,所有相关实例都在范围内是至关重要的。否则,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(togeneriinstance(Block(MathArgs“a+b”)))
将生成指令
谢谢你的快速回答,也很抱歉你的打字错误,你认为是对的,我刚把它修好。我确实有过类似的解决方案,但是它不适合我正在尝试的范围:提供一个通用文档模型,让用户定义到其他目标语言的转换。在定义DocumentBlock
时,我们必须假设StringWriter a
不可用。当试图定义一个更通用的typeclassWriter a o
并由用户进行优化时,它无法编译,因为指令不会隐含地为任何Writer a o
提供实例。(请参阅下一条评论)(我不知道它是如何工作的,因此请参阅原始文章中的代码示例)@flupe只要您还提供了一个通用的catch-all实例实例编写器a o
,供用户重叠,这些约束就不应该是问题,因为它们总是可以满足的。但是,重要的是,当必须使用特定实例时,它们必须在范围内。@flup实际上,这个约束太弱了:它只需要转换为“someo
”,这在以后将是无用的,因为我们不能依赖任何o
。对于写入
,您需要一个固定的返回类型,或者类似的东西。