通过散列实现排序

Implement Ordering via hashing

我有一组相对较大的代数数据类型,我无法自动派生 EqOrd,因为数据类型中的单个字段被视为元数据,不应被考虑在平等和有序。例如,数据类型可能如下所示:

data Foo = A Int | B String | C String Int | ... | Z String String Int 

在这种情况下,每个 Int 都是元数据。

所以我所做的是通过比较构造函数手动实现 Eq。但是对于 Ord 这变得疯狂,因为如果我有 n 构造函数,我必须实现 n^2 比较函数。所以目前我的工作是手动实现 Hashable 这需要我为每个构造函数实现一个散列函数。然后在我的 Ord 实例中进行哈希比较。

这显然有一些问题,因为 compare (hash x) (hash y) == EQ -> x == y 不成立,因为两个不同的值可以共享相同的散列。然而,这可以通过首先手动检查是否相等来处理,如果是这种情况,总是说左侧比右侧小。

但是现在对于任何类型的某些值,它都持有 a < b && b < a。我不确定在 Haskell Ord 实例中是否允许。所以基本上我的问题是,是否可以像这样实施 Ord?我需要 Ord 的原因是因为许多库需要 Ord。例如图形库和地图库。

这是一个完整的例子:

{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

module Test where

import Prelude

import Data.Bits (xor)
import Data.Hashable (Hashable (..))

data Foo = A Int | B String | C String Int | Z String String Int

instance Eq Foo where
    (A _) == (A _)             = True
    (B x1) == (B x2)           = x1 == x2
    (C x1 _) == (C x2 _)       = x1 == x2
    (Z x1 y1 _) == (Z x2 y2 _) = x1 == x2 && y1 == y2
    _ == _                     = False

instance Hashable Foo where
    hashWithSalt s (A _)     = s `xor` (hash @Int 1)
    hashWithSalt s (B x)     = s `xor` (hash @Int 2) `xor` (hash x)
    hashWithSalt s (C x _)   = s `xor` (hash @Int 3) `xor` (hash x)
    hashWithSalt s (Z x y _) = s `xor` (hash @Int 4) `xor` (hash x) `xor` (hash y)

instance Ord Foo where
    compare (hash -> a) (hash -> b) = case compare a b of
                                        EQ -> if a == b then EQ else LT
                                        e -> e

好吧,这比我实际写出来时预期的要复杂一些,所以也许有人可以想出更简单的东西,但是...

如果您可以自由地修改您的类型,我建议您在有问题的整数类型中使您的类型多态并派生一个仿函数:

{-# LANGUAGE DeriveFunctor #-}
data FooF int = A int | B String | C String int | Z String String int deriving (Functor)

现在,您的原始类型由别名指定:

type Foo = FooF Int

您可以使用独立的派生子句为 FooF () 派生 EqOrd:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE StandaloneDeriving #-}
deriving instance Eq (FooF ())
deriving instance Ord (FooF ())

然后使用忘记整数的转换函数:

forgetInts :: Foo -> FooF ()
forgetInts x = () <$ x

您可以编写 Foo 个实例,如下所示:

import Data.Function
instance Eq Foo where
  (==) = (==) `on` forgetInts
instance Ord Foo where
  compare = compare `on` forgetInts

一个缺点是您可能需要一些额外的类型签名或注释,因为 A 10 不再明确地 FooF Int 而不是 FooF Double。例如,参见下面的 main

完整代码:

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE StandaloneDeriving #-}

import Data.Function

data FooF int = A int | B String | C String int | Z String String int deriving (Functor)
type Foo = FooF Int
deriving instance Eq (FooF ())
deriving instance Ord (FooF ())

forgetInts :: Foo -> FooF ()
forgetInts x = () <$ x

instance Eq Foo where
  (==) = (==) `on` forgetInts
instance Ord Foo where
  compare = compare `on` forgetInts

main = do
  print $ Z "foo" "bar" 1 == (Z "foo" "bar" 2 :: Foo)
  print $ compare (A 10) (A 20 :: Foo)

这是一个无哈希解决方案,即使您有多种元数据类型也可能有效(我单独发布的 Functor 答案无效)。如果您可以灵活地将元数据包装在 newtype 中,则可以使用 EqOrd 新类型的实例到 "shield" 来自派生 Eq 的元数据] 和 Ord:

-- Meta data is always equal
newtype Meta a = Meta a
instance Eq (Meta a) where
  x == y = True
  x /= y = False
instance Ord (Meta a) where
  compare x y = EQ

然后,像这样的类型:

data Foo = A (Meta Int) | B String | C String (Meta Bool) 
  | Z String String (Meta String) deriving (Eq, Ord)

与派生的 EqOrd 实例进行比较,就好像元数据不存在一样:

main = do
  print $ Z "foo" "bar" (Meta "different") == Z "foo" "bar" (Meta "but still the same")
  print $ compare (A (Meta 10)) (A (Meta 20))

这里的缺点是新类型包装器的常见问题:您需要包装和解包(或 coerce)元数据。

完整代码:

newtype Meta a = Meta a
instance Eq (Meta a) where
  x == y = True
  x /= y = False
instance Ord (Meta a) where
  compare x y = EQ

data Foo = A (Meta Int) | B String | C String (Meta Bool)
  | Z String String (Meta String) deriving (Eq, Ord)

main = do
  print $ Z "foo" "bar" (Meta "different") == Z "foo" "bar" (Meta "but still the same")
  print $ compare (A (Meta 10)) (A (Meta 20))