26

私のプロジェクトでは、いくつかのタイプの値の1つを保持できるデータ型を作成しました。

data PhpValue = VoidValue | IntValue Integer | BoolValue Bool

私が今やりたかったのは、PhpValue型の2つの値が同じコンストラクターであるかどうかを簡単にチェックする方法です(ここでの用語と混同している場合は訂正してください。ただし、基本的に両方が同じであるかどうかを確認したいのは、たとえば、IntValue特定の値を気にせずに、です)。

これが私がそのために書いた関数です:

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

これは正常に機能しますが、あまり好きではありません。コンストラクターを追加すると(のようにFloatValue Float)、関数を書き直す必要があり、データ定義が大きくなるにつれて大きくなります。

質問:コンストラクターを追加しても実装が変更されないように、そのような関数を作成する方法はありますか?

記録のために:定義を変更したくないのでdata、コードの残りの部分には十分なモナドがあります;)

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)

onfromを使用すると、次のようにData.Function書き換えることができますsameConstructor

sameConstructor = (==) `on` toConstr

これはと同じです

sameConstructor l r = toConstr l == toConstr r

on使用しているバージョンが一目で読みやすいと思います。

于 2012-04-11T19:44:33.933 に答える
5

これは、HaskellおよびMLファミリー言語の表現問題として知られています。不十分な解決策はたくさんありますが(Data.TypeableHaskellでの型クラスの使用や乱用を含む)、良い解決策はありません。

于 2012-04-11T19:52:36.960 に答える
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デフォルトの署名であり、型がのインスタンスである場合、Dataの定義を省略してeqConstr、Tikhonの実装にフォールバックできることを意味します。

Template 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
        }   
    }  

対照的に、を使用すると、引数が等しいかどうかを比較する前に、各引数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(コンピューティングには、表示する価値のない他の多くの肥大化が関係しています)

実際には、これにより、テンプレートHaskellの実装が約2倍高速になります。

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 に答える
2

の人気のある代替手段の1つ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 に答える
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

もう1つの問題は(おそらく)、これがニュータイプを介してピアリングすることです。だからあなたが持っているなら

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 に答える