【问题标题】:Type class instance not being used when deriving containing data structure派生包含数据结构时未使用类型类实例
【发布时间】:2016-12-16 02:05:02
【问题描述】:

我一直在探索在我的代码中使用更多 newtype 包装器来创建更多不同的类型。我还使用 Read/Show 进行了很多廉价的序列化,特别是作为强类型配置文件的简单形式。我今天遇到了这个:

示例是这样开始的,我定义了一个简单的 newtype 来包裹 Int,以及一个用于展开的命名字段:

module Main where

import Debug.Trace ( trace )
import Text.Read ( readEither )


newtype Bar = Bar { unBar :: Int }
   deriving Show

自定义实例从简单的 Int 语法中读取其中之一。这里的想法是能够将“42”放入配置文件而不是“Bar { unBar = 42 }”会很棒

这个实例也有trace "logging",所以我们可以在观察问题的时候看到这个实例什么时候真正被使用。

instance Read Bar where
   readsPrec _ s = [(Bar i, "")]
      where i = read (trace ("[debug \"" ++ s ++ "\"]") s)

现在是另一种包含 Bar 的类型。这个只会自动派生读取。

data Foo = Foo { bar :: Bar }
   deriving (Read, Show)


main :: IO ()
main = do

单独反序列化 Bar 类型可以正常工作并使用上面的 Read 实例

   print $ ((readEither "42") :: Either String Bar)
   putStrLn ""

但由于某种原因,包含 Bar 并自动派生到 Read 的 Foo 并没有向下钻取并拾取 Bar 的实例! (请注意,也不会显示来自跟踪的调试消息)

   print $ ((readEither "Foo { bar = 42 }") :: Either String Foo)
   putStrLn ""

那么好吧,Bar 的默认 Show 表单怎么样,应该和默认的 Read 匹配吗?

   print $ ((readEither "Foo { bar = Bar { unBar = 42 } }") :: Either String Foo)

不!也不行!!同样,没有调试消息。

这是执行输出:

  $ stack exec readbug
  [debug "42"]
  Right (Bar {unBar = 42})

  Left "Prelude.read: no parse"

  Left "Prelude.read: no parse"

这对我来说看起来有问题,但我很想听到我做错了。

提供了上述代码的完整工作示例。在test project on darcshub 中查看文件src/Main.lhs

【问题讨论】:

  • 这是一个非常好的问题。我喜欢你让某人开始调试你的代码变得多么容易。希望我的回答有助于确定您遇到的特定问题。除此之外,我建议将Read 用于除调试以外的任何事情 - 然后确保使用read . show = id。我会将我的配置放在 JSON 中(并使用 aeson 进行编码/解码),或者(如果您坚持使用自定义解析器)使用类似 attoparsecmegaparsec 的东西。 Read 是一个非常低效的解析器,因为它愿意在任何地方回溯。
  • 你错了:Foo 的派生实例使用你写的BarRead 实例!只是Foo 实例在它费力强制Bar 的值之前失败(因此从不强制其中包含trace 的thunk),因为Bar 错误地报告它消耗了所有剩余的输入并且所以Foo 读者看不到} 它需要成功。
  • @Alec 我没有考虑过使用 JSON 进行配置。保持打字和层次结构。然后你最终得到一个可供其他语言/系统使用的配置文件。我现在用新类型来探索一下。谢谢!

标签: haskell typeclass deriving


【解决方案1】:

问题出在ReadreadsPrec 需要考虑在Bar 之后可能会看到更多内容的可能性。 Quoting the Prelude:

readsPrec d s 尝试解析字符串前面的值,返回(<parsed value>, <remaining string>) 对的列表。如果没有解析成功,则返回列表为空。

在你的情况下,你想要:

instance Read Bar where
   readsPrec d s = [ (Bar i, s') | (i, s') <- readsPrec d tracedS ]
      where tracedS = trace ("[debug \"" ++ s ++ "\"]") s

然后,以下工作:

ghci> print $ ((readEither "Foo { bar = 42 }") :: Either String Foo)
[debug " 42 }"]
Right (Foo {bar = Bar {unBar = 42}})

你的另一个问题,即:

那么好吧,Bar 的默认 Show 表单怎么样,应该和默认的 Read 匹配吗?

 print $ ((readEither "Foo { bar = Bar { unBar = 42 } }") :: Either String Foo)

是你的错:你为Bar 定义了一个Read 实例,这样read . show 就不是一个身份操作。当Foo 派生Read 时,它使用Bars Read 实例(它不会尝试重新生成Bar 如果你在其上派生Read 会生成的代码)。

【讨论】:

  • 我知道这是一种切线,但我忍不住建议它:instance Read Bar where readsPrec = coerce (readsPrec @Int)
  • @Alec 啊!我没有意识到readreadsPrec 之间的这种区别,但是使用相同的功能更有意义。谢谢。
  • @DanielWagner 哦,我确实喜欢它的简洁性。我不知道 Data.Coerce。我想知道它是否更有效率。还需要-XTypeApplications 谢谢!
  • @dino coerce 有时会产生很大的效率差异——但在这种情况下,它几乎肯定不会被注意到。
猜你喜欢
  • 1970-01-01
  • 2014-01-20
  • 1970-01-01
  • 2019-07-25
  • 2014-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-07
相关资源
最近更新 更多