Haskell 不使用类型类的更具体的实例

标签 haskell instance typeclass extensibility

在过去的几天里,我一直在弄清楚我正在尝试做的事情在 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/

相关文章:

haskell - 将一种数据类型转换为另一种数据类型

java - 单例模式实现中的类变量和方法

list - 我可以在haskell 中列出类型吗?

c# - 如何从 C# 中的字符串中获取实例?

haskell - 使用关联类型族时推断类型类约束

monads - 在 Coq 中定义 Maybe monad

haskell - Emacs 在 haskell 模式下挂起,并调用了 lower-haskell-load-file

haskell - 如何使用 Data.Reify 来具体化数据列表?

haskell - 一般将元组的每个组件放在 Maybe 中

scala - 如何让 Scala 的类型系统捕获这个 MatchError?