3

我希望能够派生EqShow用于包含多个字段的 ADT。其中之一是函数字段。这样做时Show,我希望它显示一些虚假的东西,例如"<function>";这样做时Eq,我希望它忽略该字段。Show如果不为and手写完整的实例,我怎样才能最好地做到这一点Eq

我不想将函数字段包装在 a 中newtype并编写我自己的Eq并且Show为此 - 那样使用太麻烦了。

4

4 回答 4

8

获得正确EqShow实例的一种方法是,而不是对该函数字段进行硬编码,而是将其设为类型参数并提供一个仅“擦除”该字段的函数。即,如果你有

data Foo = Foo
  { fooI :: Int
  , fooF :: Int -> Int }

你把它改成

data Foo' f = Foo
  { _fooI :: Int
  , _fooF :: f }
 deriving (Eq, Show)
type Foo = Foo' (Int -> Int)

eraseFn :: Foo -> Foo' ()
eraseFn foo = foo{ fooF = () }

然后,Foo仍然不能Eq- 或Show不能(毕竟它应该),但要使Foo值可显示,您可以将其包装在eraseFn.

于 2020-08-23T16:44:32.300 回答
6

通常我在这种情况下所做的正是您说您不想做的事情,即将函数包装在 a 中newtype并为此提供 a Show

data T1
  { f :: X -> Y
  , xs :: [String]
  , ys :: [Bool]
  }
data T2
  { f :: OpaqueFunction X Y
  , xs :: [String]
  , ys :: [Bool]
  }
  deriving (Show)

newtype OpaqueFunction a b = OpaqueFunction (a -> b)

instance Show (OpaqueFunction a b) where
  show = const "<function>"

如果您不想这样做,则可以将函数设为类型参数,并在Show输入类型时将其替换:

data T3' a
  { f :: a
  , xs :: [String]
  , ys :: [Bool]
  }
  deriving (Functor, Show)

newtype T3 = T3 (T3' (X -> Y))

data Opaque = Opaque

instance Show Opaque where
  show = const "..."

instance Show T3 where
  show (T3 t) = show (Opaque <$ t)

或者我将重构我的数据类型以Show仅派生我希望Show默认能够使用的部分,并覆盖其他部分:

data T4 = T4
  { f :: X -> Y
  , xys :: T4'     -- Move the other fields into another type.
  }

instance Show T4 where
  show (T4 f xys) = "T4 <function> " <> show xys

data T4' = T4'
  { xs :: [String]
  , ys :: [Bool]
  }
  deriving (Show)  -- Derive ‘Show’ for the showable fields.

或者,如果我的类型很小,我将使用 anewtype而不是,并通过以下方式data派生:ShowOpaqueFunction

{-# LANGUAGE DerivingVia #-}

newtype T5 = T5 (X -> Y, [String], [Bool])
  deriving (Show) via (OpaqueFunction X Y, [String], [Bool])

如果您关心保留字段名称/记录访问器,则可以使用该iso-deriving包对使用镜头的类型执行此操作。data

至于Eq(或Ord),拥有一个等同于可以以某种方式明显区分的值的实例并不是一个好主意,因为某些代码会将它们视为相同而其他代码不会,现在您不得不关心稳定性:在某些情况下我有a == b,我应该选择a还是b?这就是为什么可替代性是 : if 的一个定律的原因Eqforall x y f. (x == y) ==> (f x == f y)if是一个“公共”函数,它支持andf类型的不变量(尽管浮点数也违反了这一点)。一个更好的选择是像上面这样,仅对可以满足法律的类型的部分具有相等性,或者在使用站点显式使用比较模某个函数,例如,xyT4comparing someField.

于 2020-08-23T17:01:36.637 回答
2

中的模块Text.Show.Functionsbase显示的函数提供了一个显示实例<function>。要使用它,只需:

import Text.Show.Functions

它只是定义了一个类似的实例:

instance Show (a -> b) where
  show _ = "<function>"

同样,您可以定义自己的Eq实例:

import Text.Show.Functions

instance Eq (a -> b) where
  -- all functions are equal...
  -- ...though some are more equal than others
  _ == _ = True

data Foo = Foo Int Double (Int -> Int) deriving (Show, Eq)

main = do
  print $ Foo 1 2.0 (+1)
  print $ Foo 1 2.0 (+1) == Foo 1 2.0 (+2)  -- is True

这将是一个孤立实例,因此您会收到带有-Wall.

显然,这些实例将适用于所有功能。您可以为更专业的函数类型编写实例(例如,仅用于Int -> String,如果这是您的数据类型中的函数字段的类型),但无法同时 (1) 使用内置EqShow派生机制来派生您的数据类型的实例,(2)不为函数字段(或其他答案中提到的其他类型多态性)引入newtype包装器,以及(3)仅将函数实例应用于您的数据类型的函数字段,而不是其他同类型的函数值。

如果您真的想在没有newtype包装器的情况下限制自定义函数实例的适用性,您可能需要构建自己的基于泛型的解决方案,除非您想对大量数据类型执行此操作,否则这没有多大意义。如果你走这条路,那么Generics.Deriving.Show和中的Generics.Deriving.Eq模块generic-deriving为这些实例提供模板,可以对其进行修改以专门处理函数,允许你使用一些存根实例派生每个数据类型的实例,例如:

instance Show Foo where showsPrec = myGenericShowsPrec
instance Eq Foo where (==) = myGenericEquality
于 2020-08-23T17:04:20.170 回答
0

我提出了一个为 fieldsvia字段添加注释的想法,它允许对单个字段的行为进行操作。

data A = A
  { a :: Int
  , b :: Int
  , c :: Int -> Int via Ignore (Int->Int)
  }
  deriving
    stock GHC.Generic

  deriving (Eq, Show)
    via Generically A   -- assuming Eq (Generically A)
                        --        Show (Generically A)

但这已经可以通过“显微外科”库实现,但您可能需要编写一些样板来实现它。另一种解决方案是以“产品总和样式”编写单独的行为

data A = A Int Int (Int->Int)
  deriving
    stock GHC.Generic

  deriving
    anyclass SOP.Generic

  deriving (Eq, Show)
    via A <--> '[ '[ Int, Int, Ignore (Int->Int) ] ]
于 2021-09-01T16:13:00.447 回答