为大和类型编写一个 Hashable 实例

Writing a Hashable instance for a large sum type

我有大额型

data Value
= VNull
| VDouble !Double
| VSci !Scientific
| VInt !Int
| VText !Text
| VTexts ![Text]
| VByteString !BS.ByteString
| VUTCTime !UTCTime
-- This goes on for quite a few more lines

我需要这个数据类型的 Hashable 实例。我当然可以手动输入实例,但幸运的是,hashWithSalt 有一个基于泛型的默认实现。

不幸的是——据我所知——这需要值类型中可以是 "packed" 的任何类型都具有 Hashable 实例。嗯,UTCTime 没有。

看来我可以在两个 "suboptimal" 解决方案之间进行选择:

  1. 手动键入 Hashable 实例。
  2. 编写 Hashable UTCTime 的孤立实例

我认为应该第三种,"optimal"方式:只为值构造函数编写一个不可能自动完成的实现,即做像这样:

instance Hashable Value where
    hashWithSalt (VUTCTime t) = ... -- custom implementation
    hashWithSalt _ = ... -- use the default implementation

这个问题当然可以问得更笼统:如何在某些值构造函数的情况下重用现有的实例实现,同时在特定情况下拥有自己的实现 而不必为每个构造函数编写样板值构造函数.

我希望添加一个孤立实例。无论如何,您可以通过以下方式避免这种情况。

定义这个辅助类型

data ValueNotTime
= VNull
| VDouble !Double
| VSci !Scientific
| VInt !Int
| VText !Text
| VTexts ![Text]
| VByteString !BS.ByteString

并自动派生 Hashable。然后,写一个同构

iso :: Value -> Either ValueNotTime UTCTime
osi :: Either ValueNotTime UTCTime -> Value

以显而易见的方式。那么,

instance Hashable Value where
    hashWithSalt v = case iso v of
       Left valueNoTime -> use derived implementation (hashWithSalt valueNoTime)
       Right utcTime    -> use custom implementation

这似乎是从以下位置获取孤立实例的好地方:https://hackage.haskell.org/package/hashable-time


如果导出通用实现,比如 genericHashWithSalt(但目前不是 https://github.com/tibbe/hashable/issues/148),则可以执行

data Value_ utctime
  = ...
  | VUTCTime utctime
  deriving (Generic, Functor)
type Value = Value_ UtcTime

instance Hashable Value where
  hashWithSalt s (VUTCTime t) = (my custom implementation) s t
  hashWithSalt s v = genericHashWithSalt s (fmap (\_ -> ()) v)

如果你不想破坏你的类型,也应该可以修改 Value 的通用表示作为在调用 genericHashWithSalt 之前隐藏 VUTCTime 的另一种方式.

 data Value = ...  -- the original one

 instance Hashable Value where
   hashWithSalt s (VUTCTime t) = (my custom implementation) s t
   hashWithSalt s t = genericHashWithSalt s (genericHideLastConstructor t)
   -- something like that...

对于这种特殊情况,您应该只使用 hashable-time package,它在标准化位置定义了孤立实例。

一般情况下,我会:

  • 将有问题的类型包装在 newtype 中,这样您就可以在本地定义实例,而不会冒孤儿实例问题的风险。
  • 只写孤立实例。如果其他人不太可能提供冲突的实例(即当 class 和 type 都属于不太可能被其他人联合使用的晦涩包),那么这不是真正需要担心的事情大约(即使在某些时候会发生重复实例错误,这也很容易修复,这实际上是一件好事,消除了 newtype 会提供的冗余)。
  • 将实例添加到它最初来自的库中。如果 class 或类型来自一个非常常见的库,那么在不太常见的库中定义实例可能是有意义的。如果那是开源的,请在此处添加实例并向作者发送拉取请求。

你可以打一个"hole"的字,填上hashWithSalt的洞。所以:

{-# LANGUAGE DeriveFunctor, DeriveGeneric, DeriveAnyClass #-}
import Data.Hashable
import Data.Text (Text)
import Data.Time
import GHC.Generics
import qualified Data.ByteString as BS
data ValueF a
    = VNull
    | VDouble !Double
    | VInt !Int
    | VText !Text
    | VTexts ![Text]
    | VByteString !BS.ByteString
    | VUTCTime !a
    deriving (Hashable, Functor, Generic)

newtype Value = Value (ValueF UTCTime)

instance Hashable Value where
    hashWithSalt s (Value (VUTCTime t)) = {- whatever you're going to do here -}
    hashWithSalt s (Value v) = hashWithSalt s (() <$ v)
    -- OR
    -- hashWithSalt s (Value v) = hashWithSalt s (unsafeCoerce v :: Value ())