为什么在Haskell中需要显式推导Show/Read?

3
我们不能对每种类型都执行read someValue :: someDataTypeshow someValue,因为deriving (Show, Read)必须写在data声明中。除了错误之外,还有没有我们不希望我们的类型可序列化的情况?为什么要将Show与Read分开?除了错误之外,是否有只想显示某些数据而不想读取它的情况?如果没有,为什么不使用单个数据类型Serializable
刚才,我正在使用Gloss库的Key数据类型,它派生了Show而不是Read,这让我很困惑。这很遗憾,因为我想把控制的配置放在一个文件中,然后读取它,这样玩家就可以更改控制并拥有自己的配置。我不得不为Key、SpecialKey和MouseButton做包装器,这不是什么大问题,但是毫无意义。
data Key' = Char' Char | SpecialKey' SpecialKey | MouseButton' MouseButton
    deriving (Eq, Ord, Show, Read)
convertKey x = case x of
    Char' c -> Char c
    SpecialKey' sk -> SpecialKey sk
    MouseButton' mb -> MouseButton mb
3个回答

11

首先,不是所有的数据类型都可以被显示,例如函数无法被显示,因此并非所有的数据类型都可以被展示(也无法读取)。最初的Haskell定义指定,如果没有给出派生子句,则会派生尽可能多的派生类。这使得很难知道实际派生了哪些类,因此Haskell定义已更改为强制使用显式的派生子句。

第二,在最初的Haskell定义中,show和read函数在Text类中捆绑在一起。当您衍生Show和Read时,这不是太大的问题,但手动编写它们时会很麻烦。通常情况下,您想要定义一个特殊的show函数,但现在您被迫编写一个read函数,因此最好将它们分开。个人几乎总是派生Show,但几乎从不派生Read。

Show和Read类实际上并不是用于序列化,而是用于简单的输入和输出。


我不太了解Python,但我怀疑答案是否定的。 - augustss
这是一种可个性化的方式,将对象打印为字符串。 - L01man

10

为什么Show要与Read分开

我不知道最初的原因是什么,但我认为这种分离应该继续存在,因为有一些(非常少量)类型可以被展示或显示占位符字符串,但无法反向读回。函数是典型例子。

另一种思考方式是:将事物放在不同的类中非常容易,但在一个类中处理太多的函数并不总是在相同的上下文中合理,这是非常困难的。许多人认为Num类就是这个问题的一个主要例子。

如何读取键和其他未包含在Read中的类型

步骤一:发送补丁程序,将Read添加到派生实例集合中。步骤二:通过使用独立派生来制定解决方案:

{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show Key

第三步:使用CPP使您的代码能够与代码库的任一版本配合使用,无论是具有Ben将来发布的Read实例的Gloss库,还是没有该实例的版本。

为什么没有Serializable类?

首先,存在Serialize类。此外,文本序列化是一种可怕的方式。也许您需要更懒惰的序列化类,那么您应该查看Binary类。如果您担心性能,那么您可能会喜欢blaze-builder,尽管我并没有真正使用过它。


谢谢,我知道了。对不起,我没有完全理解在非Read类型上使用read的步骤。我应该放什么代码? - L01man
@L01man 我已经复制了上面的代码。你需要在文件顶部添加 {-# LANGUAGE ... 这一行,并且确保在文件中包含 deriving instance Show Key 这一行。 - Thomas M. DuBuisson
它仍然显示main: Prelude.read: no parse。我以为不需要步骤3就能工作... 再次抱歉;你如何完成第三步? - L01man
你能把你的代码粘贴到像hpaste这样的地方吗(或者只需编辑你的问题)?此外,如果你已经做到了这一步,那么程序肯定已经编译过并且类型检查通过了 - 所以这一点是好的。这个错误表明你的输入字符串格式不符合程序的预期。 - Thomas M. DuBuisson
你说得对,我忘记在字符串中删除一个旧的“Key”。现在它可以工作了。谢谢你的帮助。 - L01man

7

并非所有类型都可以序列化。如何在String -> StringString之间建立同构?如果您为String -> String提供ReadShow实例,我可以找到一个函数,例如:

evil :: String -> String
evil s = map succ (read s s ++ " evil")

假设:
read (show evil) = evil

我们得到了。
evil (show evil)
  = map succ (read (show evil) (show evil) ++ " evil")
  = map succ (evil (show evil) ++ " evil")
  = map succ (evil (show evil)) ++ "!fwjm"

如果定义了evil(显示邪恶),那么它有一个满足c = succ c的第一个字符c,这是不可能的。

一般来说,函数无法序列化。有时,我们编写打包函数的数据类型,因此并非每个数据类型都可序列化。例如,

data Psychiatrist
  = Listen (String -> Psychiatrist)
  | Charge Int

有时,即使对于这些类型,您可能也会选择提供Read的部分实现(缺少某些案例)和Show的实现(例如,使用函数占位符或制表符),但没有规范的方式来选择它们或者你为什么希望两者都存在。
正如其他人所提到的,严肃的序列化是Serialize的领域。我倾向于将ShowRead用于诊断目的,特别是在ghci中尝试一些东西。出于这个目的,Show远比Read有用,因为ghci有一个解析器来进行读取。

我明白了,我会回来理解你的“邪恶”函数!我也会看一下“Serialize”。 - L01man

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接