Wrapping newtypes around fields with DerivingVia

I have some datatypes with multiple fields, for which I’d like to derive Binary and similar instances, based on DeriveAnyClass and Generic. However, some field in my type doesn’t have a Binary instance, so automatic deriving fails.

I could of course replace this field with one of a newtype type wrapping the original one, but this complicates usage of my datatype quite a bit, always need to (un)wrap the newtype.

I could provide an orphan Binary instance for the field type, but that’s to be avoided. I’m aware of the generic-override package which could allow me to wrap a newtype around the field just for the instance generation, then have a Binary instance of said newtype, but this would still require me to have orphan instances, in this case for Binary (Override ...), so this just pushed the problem forward.

Am I missing something? Is there a way to wrap one or more fields (i.e., not the whole structure as a deriving via approach would allow for) with a newtype only for the derived code, without introducing any orphans at all?

1 Like

You can follow the override approach without orphans by declaring your own Override type to attach a Binary instance to.

At the same time, I think an orphan Binary (Override ...) or Binary (Generically ...) isn’t really bad.

2 Likes

If you are writing, say, an executable (instead of a public library), I don’t think orphans are that bad.

3 Likes

I have also found this to be a missing feature. Honestly, I don’t think the interface provided by DerivingVia is much good. I’d much rather have a full value-level interface. (Although the exact details of this require a lot of work.)

There’s an idea that was suggested a couple of years ago, but I don’t think it made any progress: https://github.com/ghc-proposals/ghc-proposals/pull/324

Duplicating the Override machinery seems a bit overkill :wink: As @f-a also mentions, I could go for an orphan instance (either on the type of the field directly, or indeed on Override), though it doesn’t “feel” right, as in, it feels like some should-be-standard functionality is missing.

I’m not sure how a Binary instance on Generically would work, since then the field’s type should have Generic instance, and I’d need to provide an orphan Binary instance for it (... via Generically a) anyway.

Thanks! I had seen some blog-post related to said proposal while doing my research. Indeed, being (un)able to operate on particular fields when DerivingVia seems like a rather common use-case for which no (simple) solution currently exists.

Could your generic-data-surgery library be useful here? In particular modifyRField.

For a type Foo, we can define a WrappedFoo newtype with a non-derived Generic instance equal to that of Foo but which wraps the problematic field in an adapter newtype. Then perhaps we could use DeriveVia on WrapFoo. Not sure how different would it be from the Override approach.

Just spelling out a concrete example using @Lysxia’s microsurgeries:, using Semigroup instead of Binary (but the only difference is that there is no Binary Generically instance yet, but it is added in e.g. this PR: Binary instance for Generically. by Icelandjack · Pull Request #192 · kolmodin/binary · GitHub):

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# OPTIONS_GHC -Wall #-}

import GHC.Generics (Generic)
import Generic.Data.Microsurgery

data NotASemigroupYet

newtype SemigroupWrapper a = SemigroupWrapper a

instance Semigroup (SemigroupWrapper NotASemigroupYet) where
  (<>) = undefined

data MyRecord = MyRecord
  { foo :: String,
    bar :: NotASemigroupYet
  }
  deriving stock (Generic)
  deriving
    (Semigroup)
    via (Generically (Surgery (OnField "bar" SemigroupWrapper) MyRecord))

I think this is about as good as it gets w.r.t. convenience, i.e. when you want to repeat that pattern for multiple types, you can define a type synonym for the via part, abstracting out shared parts.

5 Likes

Like a few people mentioned before: if you’re not making a library others will be importing, (like an executable or just a package you won’t be distributing) making orphan instances when needed is pretty much totally fine.

1 Like

Very nice, thank you. This should simplify things a lot.

Sure, until I run into the same problem when writing a library, or some library I depend on adds some orphan instance as well :slight_smile: I’m well aware an orphan instance would’ve been fine in my particular case, but it’s nice to know there are ways to handle the general case as well.

Indeed, I just wanted to emphasize that orphan instances aren’t “horrible and should never be created” :wink:
Good luck with your Haskell endeavors :+1: