haskellderivingderivingvia

Deriving newtype recursively like functionality


Lets say, in package P, I've got a type A, defined something like so:

newtype A = A Int

There's then external package X (which I don't control, unlike P and Q), which has a class C and an instance:

instance C Int where ...

So X depends on base.

Note P doesn't depend on X.

Now there's a package Q, which has a type B like so:

newtype B = B A

I want the following:

instance C B where ...

One way of achieving this is just adding deriving newtype clauses like so:

newtype A = A Int deriving newtype C -- in package P
newtype B = B A deriving newtype C -- in package Q

But then package P must depend on package X, all because Q want to use it in a particular way.

This is what I've been doing, but I feel like my codebase is becoming a bit of a blob. As in everything depends on everything. In somecases I've just been eliminating the newtype B entirely. But I think what is happening is my abstract utility code is becoming increasingly coupled with particular business logic, and I don't think this is a good thing.

I guess there's this approach also:

newtype B = B Int deriving newtype C -- in package Q

But then if the implementation of A changes, say like so:

newtype A = A Integer -- in package P

Then I've broken my code, as presumably what I'd now be doing to create an A is unwrapping B and wrapping it up in A.

I guess I could do this:

type A' = Int
newtype A = A A' -- in package P
newtype B = B A' deriving newtype C -- in package Q

But now things are getting a bit convoluted.

So I'm looking for alternatives to decouple P and Q, and in particular, not require P to depend on X (the package that defines C).

I'd like something like:

deriving newtype recursively C

Which pokes down unwrapping newtypes until it finds one that implements C, but I'm not sure it exists, and it probably isn't a good idea in the general case anyway (one sometimes newtypes to particularly hide certain instances that don't make sense, one does not want Num defined on timestamps, even if they are integers underneath).

So here I'm a bit stuck, and not sure the best way forward. Just looking for some options because this seems like a situation others would have encountered also.


Solution

  • Well, you can use the DerivingVia extension to write:

    newtype B = B A deriving C via Int
    

    That is, the contents of your modules can be:

    -- X.hs
    module X where
    
    class C a where
      c :: a -> String
    
    instance C Int where
      c _ = "Int"
    
    -- P.hs
    module P where
    
    newtype A = A Int
    
    -- Q.hs
    {-# LANGUAGE DerivingVia #-}
    
    module Q where
    
    import X
    import P
    
    newtype B = B A deriving C via Int
    
    main = print $ c (B (A 10))
    

    where Q depends on X and P, but P need not depend on X.

    If the representation of A changes, this will result in a compile time error which will require you to update the via clause, but I'm not sure how you could change the underlying representation of A and not expect to have to fix some usages of B in module Q.