Haskell不使用更具体的typeclass实例

Haskell不使用更具体的typeclass实例,haskell,instance,typeclass,extensibility,Haskell,Instance,Typeclass,Extensibility,在过去的几天里,我一直很难弄清楚我正在尝试做的事情在哈斯克尔是否切实可行 以下是一些背景: 我正在尝试编写一种小标记语言(类似于ReST),其中的语法已经通过指令启用了自定义扩展。 对于要实现新指令的用户,他们应该能够在文档数据类型中添加新的语义结构。例如,如果想要添加一个用于显示数学的指令,他们可能希望ast中有一个MathBlock字符串构造函数 很明显,数据类型是不可扩展的,并且存在一个通用构造函数DirectiveBlock String包含指令名称(此处为“math”)的解决方案是不可

在过去的几天里,我一直很难弄清楚我正在尝试做的事情在哈斯克尔是否切实可行

以下是一些背景: 我正在尝试编写一种小标记语言(类似于ReST),其中的语法已经通过指令启用了自定义扩展。 对于要实现新指令的用户,他们应该能够在文档数据类型中添加新的语义结构。例如,如果想要添加一个用于显示数学的指令,他们可能希望ast中有一个
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
不可用。当试图定义一个更通用的typeclass
Writer a o
并由用户进行优化时,它无法编译,因为指令不会隐含地为任何
Writer a o
提供实例。(请参阅下一条评论)(我不知道它是如何工作的,因此请参阅原始文章中的代码示例)@flupe只要您还提供了一个通用的catch-all实例
实例编写器a o
,供用户重叠,这些约束就不应该是问题,因为它们总是可以满足的。但是,重要的是,当必须使用特定实例时,它们必须在范围内。@flup实际上,这个约束太弱了:它只需要转换为“some
o
”,这在以后将是无用的,因为我们不能依赖任何
o
。对于
写入
,您需要一个固定的返回类型,或者类似的东西。