I was using a Map String (Int, Int)
where the two Int
s were used as the numerator and denominator to form a Rational
to be passed to fromList
.
Then I realized that in a point in my code I had used those two Int
s the other way around (as denominator and numerator, i.e. swapped). It took some time to find out what was wrong, so afterwards I thought that maybe I should use two dedicated types, so I wrote
newtype MyNum = MyNum Int
newtype MyDen = MyDen Int
but then I had to add a few instances for everything else to work (given the uses I make of those Int
s, I had to add deriving (Eq, Ord, Show, Read)
), and also to add some two functions to unwrap the Int
s from within the two types so that I could actually apply things like (+1)
to those wrapped Int
s.
But this means that code starts looking a bit ugly, with things like (MyNum . (+1) . unwrapMyNum)
, whereas something like (+1) <$>
would be much preferrable.
But that means that MyNum
should be a Functor
; but it can't because it's a hard type, not a type constructor.
But I don't want to make it a type constructor because I don't want to wrap anything in it other than a Int
.
Any suggestion?
I think the actual problem has nothing to do with your concrete question. Just don't use tuples, use a suitable type that expresses what both integers represent together. In this case the obvious choice would be to use Ratio Int
, with the caveat that it does not store arbitrary pairs but properly normalises the fractions (which is generally a good thing). If that's not appropriate for you, just write your own Ratio
type.
That said, there are also many things you can do to make a newtype wrapper around a single type more convenient:
Derived instances. It seems like you still used numerical operations on the wrapped type. That's easy to enable via the DerivingStrategies
extension:
{-# LANGUAGE DerivingStrategies #-}
newtype MyNum = MyNum Int
deriving stock (Eq, Ord, Show, Read)
deriving newtype (Num, Enum, Real, Integral)
and now MyNum
is basically a fully featured clone of Int
, you can directly write expressions such as negate (n + 9)
for n :: MyNum
, no need for wrapping and unwrapping. (But the compiler still baulks when you pass a MyNum
to something that expects a MyDen
.)
Mono-functor. If you rather want to emphasize MyNum
being a container for an Int
, and not a different kind of number type of its own right, then there's the MonoFunctor
class. You can instantiate
{-# LANGUAGE TypeFamilies #-}
newtype MyNum = MyNum Int
type instance Element MyNum = Int
instance MonoFunctor MyNum where
omap f (MyNum i) = MyNum (f i)
and then write e.g. omap (+1) n
, which is like (+1)<$>n
but doesn't require the container to support anything but Int
. Check out also the other classes of the package.
General wrapping / unwrapping helpers. There are alternatives for explicitly operating on newtype- or other contained data that work also when special classes are not suitable, and can be more convenient than a traditional pair of accessor and constructor. Among them are coerce
and lenses (more specifically Iso
s).