问题描述
我希望能够为包含多个字段的ADT导出Eq
和Show
。其中之一是功能字段。做Show
时,我希望它显示一些虚假的内容,例如"<function>"
;在执行Eq
时,我希望它忽略该字段。在没有为Show
和Eq
编写完整实例的情况下,如何最好地做到这一点?
我不想将函数字段包装在newtype
内,并为此编写自己的Eq
和Show
-这样使用起来太麻烦了。>
解决方法
获取正确的Eq
和Show
实例的一种方法是,将其设置为类型参数,并提供仅“擦除”该字段的功能,而不是对该功能字段进行硬编码。也就是说,如果有
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
(毕竟应该 不能),但是要{ {1}}可显示的值,您可以将其包装在Foo
中。
在这种情况下,我通常要做的就是您所说的不想要执行的操作,即将函数包装在newtype
中并提供一个Show
为此:
data T1
{ f :: X -> Y,xs :: [String],ys :: [Bool]
}
data T2
{ f :: OpaqueFunction X Y,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,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.
或者如果我的类型很小,我将使用newtype
而不是data
,并通过类似Show
的方式派生OpaqueFunction
:
{-# LANGUAGE DerivingVia #-}
newtype T5 = T5 (X -> Y,[String],[Bool])
deriving (Show) via (OpaqueFunction X Y,[Bool])
如果您关心保留字段名称/记录访问者,则可以使用iso-deriving
包针对使用镜头的data
类型执行此操作。
对于Eq
(或Ord
)来说,让实例等同于可以以某种方式观察到的值的实例不是一个好主意,因为某些代码会将它们视为相同,而其他代码不会,现在您不得不担心稳定性:在某些情况下,如果我有a == b
,应该选择a
还是b
?这就是为什么可替代性是Eq
的定律的原因:forall x y f. (x == y) ==> (f x == f y)
如果f
是支持{{1}类型不变性的“公共”函数}和x
(尽管浮点数也违反了这一点)。更好的选择是类似于上面的y
之类的东西,仅对满足规则的类型的部分具有相等性,或者在使用位置的某些功能(例如T4
)上显式使用比较模。
Text.Show.Functions
中的模块base
为显示<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)使用内置函数Eq
和Show
的派生机制来为您的数据类型派生实例,(2)不为函数字段引入newtype
包装器(或其他答案中提到的某种其他类型的多态性),和(3)仅将函数实例应用于您数据类型的function字段,而不适用于相同类型的其他函数值。
如果您确实想在没有newtype
包装器的情况下限制自定义函数实例的适用性,则可能需要构建自己的基于泛型的解决方案,除非您想这样做,否则就没有多大意义了。这适用于很多数据类型。如果走这条路,那么Generics.Deriving.Show
中的Generics.Deriving.Eq
和generic-deriving
模块将为这些实例提供模板,可以对其进行修改以专门处理函数,从而允许您使用某些实例来导出每个数据类型的实例。存根实例类似:
instance Show Foo where showsPrec = myGenericShowsPrec
instance Eq Foo where (==) = myGenericEquality