Deriving Eq and Show for an ADT that contains fields that can't have Eq or Show

Viewed 273

I'd like to be able to derive Eq and Show for an ADT that contains multiple fields. One of them is a function field. When doing Show, I'd like it to display something bogus, like e.g. "<function>"; when doing Eq, I'd like it to ignore that field. How can I best do this without hand-writing a full instance for Show and Eq?

I don't want to wrap the function field inside a newtype and write my own Eq and Show for that - it would be too bothersome to use like that.

4 Answers

One way you can get proper Eq and Show instances is to, instead of hard-coding that function field, make it a type parameter and provide a function that just “erases” that field. I.e., if you have

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

you change it to

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

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

Then, Foo will still not be Eq- or Showable (which after all it shouldn't be), but to make a Foo value showable you can just wrap it in eraseFn.

Typically what I do in this circumstance is exactly what you say you don’t want to do, namely, wrap the function in a newtype and provide a Show for that:

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>"

If you don’t want to do that, you can instead make the function a type parameter, and substitute it out when Showing the type:

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)

Or I’ll refactor my data type to derive Show only for the parts I want to be Showable by default, and override the other parts:

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.

Or if my type is small, I’ll use a newtype instead of data, and derive Show via something like OpaqueFunction:

{-# LANGUAGE DerivingVia #-}

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

You can use the iso-deriving package to do this for data types using lenses if you care about keeping the field names / record accessors.

As for Eq (or Ord), it’s not a good idea to have an instance that equates values that can be observably distinguished in some way, since some code will treat them as identical and other code will not, and now you’re forced to care about stability: in some circumstance where I have a == b, should I pick a or b? This is why substitutability is a law for Eq: forall x y f. (x == y) ==> (f x == f y) if f is a “public” function that upholds the invariants of the type of x and y (although floating-point also violates this). A better choice is something like T4 above, having equality only for the parts of a type that can satisfy the laws, or explicitly using comparison modulo some function at use sites, e.g., comparing someField.

The module Text.Show.Functions in base provides a show instance for functions that displays <function>. To use it, just:

import Text.Show.Functions

It just defines an instance something like:

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

Similarly, you can define your own Eq instance:

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

This will be an orphan instance, so you'll get a warning with -Wall.

Obviously, these instances will apply to all functions. You can write instances for a more specialized function type (e.g., only for Int -> String, if that's the type of the function field in your data type), but there is no way to simultaneously (1) use the built-in Eq and Show deriving mechanisms to derive instances for your datatype, (2) not introduce a newtype wrapper for the function field (or some other type polymorphism as mentioned in the other answers), and (3) only have the function instances apply to the function field of your data type and not other function values of the same type.

If you really want to limit applicability of the custom function instances without a newtype wrapper, you'd probably need to build your own generics-based solution, which wouldn't make much sense unless you wanted to do this for a lot of data types. If you go this route, then the Generics.Deriving.Show and Generics.Deriving.Eq modules in generic-deriving provide templates for these instances which could be modified to treat functions specially, allowing you to derive per-datatype instances using some stub instances something like:

instance Show Foo where showsPrec = myGenericShowsPrec
instance Eq Foo where (==) = myGenericEquality

I proposed an idea for adding annotations to fields via fields, that allows operating on behaviour of individual 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)

But this is already possible with the "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) ] ]
Related