为包含不能有 Eq 或 Show 的字段的 ADT 派生 Eq 和 Show
Deriving Eq and Show for an ADT that contains fields that can't have Eq or Show
我希望能够为包含多个字段的 ADT 导出 Eq
和 Show
。其中之一是功能字段。在做 Show
时,我希望它显示一些虚假的东西,例如"<function>"
;在执行 Eq
时,我希望它忽略该字段。如果不手写 Show
和 Eq
的完整实例,我怎样才能最好地做到这一点?
我不想将函数字段包装在 newtype
中并为此编写我自己的 Eq
和 Show
- 这样使用太麻烦了。
获得正确 Eq
和 Show
实例的一种方法是,而不是 hard-coding 那个函数字段,使它成为一个类型参数并提供一个函数,它只是“擦除” ” 那个领域。即,如果您有
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
.
中
通常我在这种情况下所做的正是你所说的你不想做的,即将函数包装在newtype
中并提供一个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.
或者如果我的类型很小,我将使用 newtype
而不是 data
,并通过类似 OpaqueFunction
:[=39= 的方式派生 Show
]
{-# 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
?这就是为什么 substitutability 是 Eq
的定律: forall x y f. (x == y) ==> (f x == f y)
如果 f
是一个“public” 函数,它支持不变量x
和 y
的类型(尽管 floating-point 也违反了这一点)。更好的选择是类似于上面的 T4
,仅对满足法律的类型部分具有相等性,或者在使用位置显式地使用比较模某些函数,例如 comparing someField
.
base
中的模块Text.Show.Functions
为显示<function>
的函数提供了一个show实例。要使用它,只需:
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) 使用 built-in Eq
和 Show
派生机制来派生数据类型的实例,(2) 不为函数字段引入 newtype
包装器(或其他答案中提到的其他类型多态性) ),以及 (3) 仅将函数实例应用于数据类型的函数字段,而不是相同类型的其他函数值。
如果你真的想在没有 newtype
包装器的情况下限制自定义函数实例的适用性,你可能需要构建你自己的 generics-based 解决方案,这没有多大意义,除非你想为很多数据类型做这个。如果你走这条路,那么 generic-deriving
中的 Generics.Deriving.Show
和 Generics.Deriving.Eq
模块为这些实例提供模板,可以修改这些模板以特殊处理函数,允许你派生 per-datatype 个实例使用一些存根实例,例如:
instance Show Foo where showsPrec = myGenericShowsPrec
instance Eq Foo where (==) = myGenericEquality
我提出了一个向字段添加注释的想法 via
fields,它允许对各个字段的行为进行操作。
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)
但这已经可以通过 "microsurgery" library, but you might have to write some boilerplate to get it going. Another solution is to write separate behaviour in "sums-of-products style"
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) ] ]
我希望能够为包含多个字段的 ADT 导出 Eq
和 Show
。其中之一是功能字段。在做 Show
时,我希望它显示一些虚假的东西,例如"<function>"
;在执行 Eq
时,我希望它忽略该字段。如果不手写 Show
和 Eq
的完整实例,我怎样才能最好地做到这一点?
我不想将函数字段包装在 newtype
中并为此编写我自己的 Eq
和 Show
- 这样使用太麻烦了。
获得正确 Eq
和 Show
实例的一种方法是,而不是 hard-coding 那个函数字段,使它成为一个类型参数并提供一个函数,它只是“擦除” ” 那个领域。即,如果您有
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
.
通常我在这种情况下所做的正是你所说的你不想做的,即将函数包装在newtype
中并提供一个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.
或者如果我的类型很小,我将使用 newtype
而不是 data
,并通过类似 OpaqueFunction
:[=39= 的方式派生 Show
]
{-# 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
?这就是为什么 substitutability 是 Eq
的定律: forall x y f. (x == y) ==> (f x == f y)
如果 f
是一个“public” 函数,它支持不变量x
和 y
的类型(尽管 floating-point 也违反了这一点)。更好的选择是类似于上面的 T4
,仅对满足法律的类型部分具有相等性,或者在使用位置显式地使用比较模某些函数,例如 comparing someField
.
base
中的模块Text.Show.Functions
为显示<function>
的函数提供了一个show实例。要使用它,只需:
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) 使用 built-in Eq
和 Show
派生机制来派生数据类型的实例,(2) 不为函数字段引入 newtype
包装器(或其他答案中提到的其他类型多态性) ),以及 (3) 仅将函数实例应用于数据类型的函数字段,而不是相同类型的其他函数值。
如果你真的想在没有 newtype
包装器的情况下限制自定义函数实例的适用性,你可能需要构建你自己的 generics-based 解决方案,这没有多大意义,除非你想为很多数据类型做这个。如果你走这条路,那么 generic-deriving
中的 Generics.Deriving.Show
和 Generics.Deriving.Eq
模块为这些实例提供模板,可以修改这些模板以特殊处理函数,允许你派生 per-datatype 个实例使用一些存根实例,例如:
instance Show Foo where showsPrec = myGenericShowsPrec
instance Eq Foo where (==) = myGenericEquality
我提出了一个向字段添加注释的想法 via
fields,它允许对各个字段的行为进行操作。
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)
但这已经可以通过 "microsurgery" library, but you might have to write some boilerplate to get it going. Another solution is to write separate behaviour in "sums-of-products style"
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) ] ]