3

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.

Iceland_jack
  • 6,848
  • 7
  • 37
  • 46
cheater
  • 347
  • 2
  • 8
  • 2
    Well... _don't do this_. If the type contains a function, it _can't_ be shown or equality-compared, pretending otherwise will just lead to confusing suprises. – leftaroundabout Aug 23 '20 at 16:02
  • @leftaroundabout that's a matter of personal preference for my own codebase. Especially Show isn't going to lead to any surprises, it's only for the programmer's use during an interactive session. It's not for serialization. – cheater Aug 23 '20 at 16:24
  • Fair enough, but if you want to play by your own rules you'll need to write your own instance too. Don't expect community/libraries/tools to help you with something the community considers a bad idea... – leftaroundabout Aug 23 '20 at 16:33
  • @leftaroundabout sorry, the narrative of objectively bad and good ideas, or that your tastes represent the whole of the community, or that the community should decide what's good for me, it all goes way too far for me. That's overstepping. – cheater Aug 23 '20 at 19:06

4 Answers4

8

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.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • This is interesting. I guess once you have a value with a function inside you can just do myVal { _fooF = () } to get a printable version. The issue with this is that it implies Foo should be polymorphic in f, whereas it really shouldn't be, it should only accept one type there. – cheater Aug 23 '20 at 18:22
  • Well, it doesn't need to be “publicly polymorphic” – you can export only the type `Foo` but not `Foo'`, then it looks as if the function field is hard-coded. – leftaroundabout Aug 23 '20 at 21:29
6

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.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166
  • This is pretty cool, especially `T5`. Is it possible to use this on ADTs that don't use GADT extensions? I guess T5 isn't polymorphic? (which is what I want tbh) – cheater Aug 23 '20 at 18:31
  • @cheater: Yeah, if I understand you correctly, you can use `DerivingVia` on polymorphic types, and even many GADTs with `StandaloneDeriving`. The `via` deriving strategy just requires `Coercible` instances, which are automatically derived for `newtype`s that are representationally equal. There are some deficiencies in the “role” system that enables coercions, where the compiler can’t prove a coercion safe, so you may sometimes need to tweak your types, or use `QuantifiedConstraints` to talk about higher-order types, e.g. `forall a b. Coercible a b => Coercible (f a) (f b)`. – Jon Purdy Aug 23 '20 at 18:37
  • I just realized I forgot that I need multiple constructors on this type. So it won't fit inside a `newtype`. How would you do something like `T5` for an ADT with two constructors, and no type parameters? Would you be kind enough to append your answer? – cheater Aug 23 '20 at 22:51
  • @cheater: If you want to use `DerivingVia` directly, you’d need to use something like `newtype X = X (Either Y Z) deriving (…) via (Either Y' Z')`, or `iso-deriving` with a `data` type. I’ll see about adding an example of the latter, at least. I hope that in the future we can derive more things structurally for `data` types. At the moment you’d have to build a decent amount of the machinery yourself through `Generic`, with techniques like those described in [Mirror Mirror: Reflection and Encoding Via](https://www.parsonsmatt.org/2020/02/04/mirror_mirror.html). – Jon Purdy Aug 24 '20 at 17:34
  • writing stuff for `data` sounds really painful - I think your `newtype ... Either` suggestion might be better for now. I do hope at least basic `data` types can be supported better in the near future. – cheater Aug 25 '20 at 02:03
  • I've accepted your answer right now for being by far the best, and I hope if `data` support materializes you'll update the answer :) – cheater Aug 25 '20 at 02:04
  • 1
    QuickCheck has a really general version of that newtype: `newtype Hide a = Hide a`. Then `instance Show (Hide a) where --something uninformative`. So `Hide (a -> b)` is a function with a `Show` instance. If the function isn't polymorphic, it might make more sense to use an instance that shows its type: `newtype ShowByType a = ShowByType a` with `instance Typeable a => Show (ShowByType a) where -- show the TypeRep in context`. – dfeuer Sep 01 '21 at 18:58
2

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
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • 3
    The problem with “optional instance” modules is that they're viral, anyone downstream will then also have the questionable blessing of that instance being in scope. (Yay, communism... the pigs would rejoice.) – leftaroundabout Aug 23 '20 at 17:18
  • this is actually a pretty good trick for when I'll want to debug something, I'll just add "deriving Show" to the right type and add the import up top. Great stuff! Not sure about the virality argument, but when debugging like this there's no need to keep this code around. – cheater Aug 23 '20 at 19:08
  • If I had my way, GHC.Show would declare `class Show a where type HahaNoOverlap a; type HahaNoOverlap = (); ....` and `instance Unsatisfiable "You can't show functions!" => Show (a -> b) where showsPrec = exfalso`. But I'm a very strict kind of guy. – dfeuer Sep 01 '21 at 22:27
2

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) ] ]
Iceland_jack
  • 6,848
  • 7
  • 37
  • 46