26

在我的项目中,我创建了一个数据类型,它可以保存以下几种类型的值之一:

data PhpValue = VoidValue | IntValue Integer | BoolValue Bool

我现在想做的是有一种简单的方法来检查PhpValue类型的两个值是否属于同一个构造函数(如果我对这里的术语感到困惑,请纠正我,但基本上我想检查两者是否都是,例如, are IntValue,而不关心特定值)。

这是我为此编写的一个函数:

sameConstructor :: PhpValue -> PhpValue -> Bool
sameConstructor VoidValue VoidValue = True
sameConstructor (IntValue _) (IntValue _) = True
sameConstructor (BoolValue _) (BoolValue _) = True
sameConstructor _ _ = False

这可以正常工作,但我不太喜欢它:如果我添加更多构造函数(如FloatValue Float),我将不得不重写该函数,并且随着我的数据定义变得更大,它会变得更大。

问题:有没有办法编写这样一个函数,这样当我添加更多构造函数时它的实现不会改变?

作为记录:我不想更改data定义,我的其余代码中有足够的 Monads ;)

4

6 回答 6

26

看看Data.Data它的toConstr功能。这将返回构造函数的表示形式,可以比较其是否相等。

通过扩展(您可以放在{-# LANGUAGE DeriveDataTypeable #-}模块的顶部),您可以Data自动为您派生一个实例:

data PhpValue = VoidValue | IntValue Integer | BoolValue Bool 
              deriving (Typeable, Data)

然后,您应该能够使用该toConstr函数通过构造函数进行比较。

现在以下将是正确的:

toConstr (BoolValue True) == toConstr (BoolValue False)

使用onfromData.Function你现在可以重写sameConstructor为:

sameConstructor = (==) `on` toConstr

这与

sameConstructor l r = toConstr l == toConstr r

我认为使用的版本on一目了然。

于 2012-04-11T19:44:33.933 回答
5

这被称为 Haskell 和 ML 系列语言中的表达问题;有许多不令人满意的解决方案(包括Data.Typeable在 Haskell 中使用和滥用类型类),但没有好的解决方案。

于 2012-04-11T19:52:36.960 回答
2

一种流行的替代方法DataGeneric. 我认为Data在这种情况下可能更有意义,但我认为为了完整性而添加它是有意义的。

{-# LANGUAGE DefaultSignatures, TypeOperators, FlexibleContexts #-}
module SameConstr where

import GHC.Generics
import Data.Function (on)

class EqC a where
    eqConstr :: a -> a -> Bool
    default eqConstr :: (Generic a, GEqC (Rep a)) => a -> a -> Bool
    eqConstr = geqConstr `on` from

class GEqC f where
  geqConstr :: f p -> f p -> Bool
  {-# INLINE geqConstr #-}
  geqConstr _ _ = True

instance GEqC f => GEqC (M1 i c f) where
  {-# INLINE geqConstr #-}
  geqConstr (M1 x) (M1 y) = geqConstr x y

instance GEqC (K1 i c)
instance GEqC (f :*: g)
instance GEqC U1
instance GEqC V1

instance (GEqC f, GEqC g) => GEqC (f :+: g) where
  {-# INLINE geqConstr #-}
  geqConstr (L1 x) (L1 y) = geqConstr x y
  geqConstr (R1 x) (R1 y) = geqConstr x y
  geqConstr _ _ = False
于 2017-08-01T17:29:43.603 回答
2

由于定义遵循常规格式,您可以使用 Template Haskell 为任何数据类型自动派生此类函数。我继续为此编写了一个简单的包,因为我对现有的解决方案并不完全满意。

首先,我们定义一个类

class EqC a where
    eqConstr :: a -> a -> Bool
    default eqConstr :: Data a => a -> a -> Bool
    eqConstr = (==) `on` toConstr

然后是一个deriveEqC :: Name -> DecsQ会自动为我们生成实例的函数。

默认签名,意味着当类型是 的实例时,default我们可以省略 的定义,并回退到 Tikhon 的实现。DataeqConstr

模板 Haskell 的好处是它产生了更有效的函数。我们可以编写$(deriveEqC ''PhpValue)并获得一个与我们手动编写的完全相同的实例。看一下生成的核心:

$fEqCPhpValue_$ceqConstr =
  \ ds ds1 ->
    case ds of _ { 
      VoidValue ->
        case ds1 of _ { 
          __DEFAULT -> False;
          VoidValue -> True
        };  
      IntValue ds2 ->
        case ds1 of _ { 
          __DEFAULT -> False;
          IntValue ds3 -> True
        };  
      BoolValue ds2 ->
        case ds1 of _ { 
          __DEFAULT -> False;
          BoolValue ds3 -> True
        }   
    }  

相比之下, using在比较它们是否相等之前通过具体化每个参数Data的显式来引入大量额外的间接:Constr

eqConstrDefault =
  \ @ a $dData eta eta1 ->
    let {
      f
      f = toConstr $dData } in
    case f eta of _ { Constr ds ds1 ds2 ds3 ds4 ->
    case f eta1 of _ { Constr ds5 ds6 ds7 ds8 ds9 ->
    $fEqConstr_$c==1 ds ds5
    }
    }

(计算中还有很多其他的膨胀toConstr,不值得展示)

在实践中,这导致 Template Haskell 实现的速度大约是两倍:

benchmarking EqC/TH
time                 6.906 ns   (6.896 ns .. 6.915 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 6.903 ns   (6.891 ns .. 6.919 ns)
std dev              45.20 ps   (32.80 ps .. 63.00 ps)

benchmarking EqC/Data
time                 14.80 ns   (14.77 ns .. 14.82 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 14.79 ns   (14.77 ns .. 14.81 ns)
std dev              60.17 ps   (43.12 ps .. 93.73 ps)
于 2017-07-30T04:51:56.803 回答
0

在您的特殊情况下,您可以使用Show编译器的魔力:

data PhpValue = VoidValue | IntValue Integer | BoolValue Bool deriving Show

sameConstructor v1 v2 = cs v1 == cs v2 where 
   cs = takeWhile (/= ' ') . show

当然取决于编译器生成的字符串表示非常接近黑客......

于 2012-04-11T21:50:07.503 回答
0

如果您不想在其他答案中使用任何合理的方式,则可以使用完全不受支持的方式,这种方式可以保证快速,但实际上不能保证给出正确的结果,甚至不会崩溃。请注意,这甚至会很乐意尝试比较函数,因为它会给出完全虚假的结果。

{-# language MagicHash, BangPatterns #-}

module DangerZone where

import GHC.Exts (Int (..), dataToTag#)
import Data.Function (on)

{-# INLINE getTag #-}
getTag :: a -> Int
getTag !a = I# (dataToTag a)

sameConstr :: a -> a -> Bool
sameConstr = (==) `on` getTag

另一个问题(可以说)是它通过新类型对等。所以如果你有

newtype Foo a = Foo (Maybe a)

然后

sameConstr (Foo (Just 3)) (Foo Nothing) == False

即使它们是使用Foo构造函数构建的。您可以通过使用 中的一些机制来解决此问题GHC.Generics,但没有与使用未优化的泛型相关的运行时成本。这变得很毛茸茸!

{-# language MagicHash, BangPatterns, TypeFamilies, DataKinds,
             ScopedTypeVariables, DefaultSignatures #-}

import Data.Proxy (Proxy (..))
import GHC.Generics
import Data.Function (on)
import GHC.Exts (Int (..), dataToTag#)

--Define getTag as above

class EqC a where
  eqConstr :: a -> a -> Bool
  default eqConstr :: forall i q r s nt f.
                      ( Generic a
                      , Rep a ~ M1 i ('MetaData q r s nt) f
                      , GNT nt)
                   => a -> a -> Bool
  eqConstr = genEqConstr

-- This is separated out to work around a bug in GHC 8.0
genEqConstr :: forall a i q r s nt f.
                      ( Generic a
                      , Rep a ~ M1 i ('MetaData q r s nt) f
                      , GNT nt)
                   => a -> a -> Bool
genEqConstr = (==) `on` modGetTag (Proxy :: Proxy nt)

class GNT (x :: Bool) where
  modGetTag :: proxy x -> a -> Int

instance GNT 'True where
  modGetTag _ _ = 0

instance GNT 'False where
  modGetTag _ a = getTag a

这里的关键思想是我们查看与类型的通用表示相关联的类型级元数据,以确定它是否是新类型。如果是,我们将其“标签”报告为0; 否则我们使用它的实际标签。

于 2017-08-01T23:39:27.120 回答