[Haskell-cafe] Alternative to newtypes or orphan instances

Clinton Mead clintonmead at gmail.com
Mon Aug 24 07:18:14 UTC 2015


Lets say there's a type `T` I've imported from a package I don't control.

I can easily write new functions to work on `T`.

For example, I could write:

f :: T -> T -> T
f x y = ...

I've extended T's functionality. No need for newtypes. This makes sense.

Whilst a newtype in the following case:

newtype Time = Time Int

because in this case `Time` is conceptually different to `Int`. For
example, you shouldn't multiply two `Time`s

So in summary, we've just extended T's functionality appropriately, without
introducing a newtype.

But this could be dangerous. Lets say another module writer writes a
similar function "f". Then if someone imports their module and ours, there
will be a clash. Fortunately the compiler tells about this when it happens,
and we can then use qualified imports to workaround the clash. This I'll
get back to later.

Now lets say I want to extend T's functionality some more, but this time by
making it an instance of class C.

Class C already exists in a package I don't control.

Now it would be unreasonable for every class writer to write instances for
every possible data type that is appropriate for their class. As you can
see, it's an O(n^2) problem, so there will undoubtably be situations where
one needs to write instances for data types and classes in separate
packages.

One recommended approach here is to newtype T, like the following:

newtype MyT = MyT T

however I find this incredibly ugly. Conceptually, there is no "newtype",
and now there's different types of Ts bashing around which really are the
same thing. Not only that, they're incompatible, for no good reason.
Instead of these different Ts being different types because they're
conceptually different things that shouldn't be mixed, the type of these
objects depends solely on what operations I want to apply to them. Imagine
a rule where:

f :: T -> T -> T

was only legal in the module T was defined. If you wanted to define T
anywhere else, you'd have to do:

f :: MyT -> MyT -> MyT

That's how messy the newtype solution is, it wouldn't be accepted for
ordinary functions, and I don't think it's reasonable to apply that
messiness to class functions.

A second approach is an orphan instance. The recommendation here is to put
the orphan instances in their own module, so the user can choose to import
them.

This may works ok if your user is writing an executable. But what if your
user is writing a library themselves. But once, you, or your user, directly
uses one of the instances, they need to import it, and they pollute the
global instance namespace for anyone that uses their package.

This is okay if you're the only person doing it, but I think it's
reasonable to say that if your practices would be bad if someone else was
doing them also, then they're a bad practice. Indeed, if someone else
writes their own instances and they clash with yours, people simply can't
use your package and their package in the same program. This is bad.

In the simple "function" case. We deal with the clash of two functions
named "f" using qualified imports. We can't do that instances, nor can we
even hide one of the instances.

So to recap here we've tried:

(1) Copying the data type. We've decided this has lots of problems.
(2) Just making an orphan instance. This has serious problems too.

I want to suggest a third option:

(3) Copying the class.

What would this involve:

a) Making a new class (say C1), with the same class methods as the existing
one (say C).
b) Making an instance of the class like so:

instance (C x) => C1 x where
  f = ModuleC.f
  ...

which forwards all the calls to C.

c) Add your new instances to C1 (these will overlap with the above but they
will be more specific so with overlapping instances this is allowed).

Now if your users want to use your instances, they import C1. They can
still use the data type T as usual on the old instances, or on any existing
functions defined on T directly.

The big question is, what if someone else does this, and creates C2?

Then they'll be a clash. But in this case, classes, unlike instances, can
be explicitly imported and qualified. Your packages won't be impossible to
use together, just the user will have to import one of them qualified.
That's acceptable, we understand we have to do this sometimes with clashing
function names too.

So I guess my questions are:

What do you think of my analysis? Are there parts that are incorrect?
What do you think of my solution? Is there reasons why it is not as good as
the "newtype" or "orphan instances" approach?
Is there a way to make my solution easier to implement, i.e. less typing?
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/haskell-cafe/attachments/20150824/be27842b/attachment.html>


More information about the Haskell-Cafe mailing list