Search code examples
haskellfunctional-programmingtype-safetynewtype

What is the proper way of wrapping an Int (not a general type) in another type if type safety is the only motive?


I was using a Map String (Int, Int) where the two Ints 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 Ints 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 Ints, I had to add deriving (Eq, Ord, Show, Read)), and also to add some two functions to unwrap the Ints from within the two types so that I could actually apply things like (+1) to those wrapped Ints.

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?


Solution

  • 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 Isos).