Hi there !
TL;DR
Which is best and why ?
- Practice 1 : Lots of newtypes, but nice API
newtype Second a = Second a
deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real, RealFrac, RealFloat)
newtype Minute a = Minute a
deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real, RealFrac, RealFloat)
newtype Gram a = Gram a
deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real, RealFrac, RealFloat)
-- Hundreds of newtypes like this
- Practice 2 : One newtype, but ugly API
newtype Qty u a = Qty a
deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real, RealFrac, RealFloat)
data Second
data Minute
data Second
-- hundreds of types like this
Some more context
I would like to ask some advice on the design of a library I wrote a year ago: convert-units: Statically typed unit conversions
Basically, the library declares some newtypes like
newtype Second a = Second a
newtype Minute a = Minute a
Then, we use the newtype information to explain how to convert it (I’m oversimplifying to get to the point, see the actual library for better code)
class (IsUnit u, IsUnit (BaseUnitOf u)) => ConvertibleUnit u a where
type family BaseUnitOf u
toBaseUnit :: u a -> BaseUnitOf u a
fromBaseUnit :: BaseUnitOf u a -> u a
instance Fractional a => ConvertibleUnit Minute a where
type instance BaseUnitOf Minute = Second
toBaseUnit (Minute a) = Second (60 *a)
fromBaseUnit(Second a) = Minute (a/60)
An alternative design (see for instance units: A domain-specific type system for dimensional analysis) would be to declare a single newtype
newtype Qty u a = Qty a
And then units as types
data Second
data Minute
instance Fractional a => ConvertibleUnit Minute a where
type instance BaseUnitOf Minute = Second
toBaseUnit (Quantity a) = Quantity (60 *a)
fromBaseUnit(Quantity a) = Quantity (a/60)
I just implemented this second design for convert-units and I’m hesitating whether to keep the old or the new design.
But now quantity types look like Qty Minute Float, which is uglier than Minute Float.
Initially, I thought that using one Qty newtype was “the right thing to do”, because it avoids to declare hundreds of newtypes which all have a dozen derived instance. But is it really bad ? Of course that’s heavier for GHC, but if it is the only argument, the current API (without the Qty newtype) is way nicer : less noise in errors, less noise when writing type.
So my questions are : how bad is it to have all these newtypes ? Can this cause problems, like heavily slowing down compilation ? What would you chose if you were me ?
Cheers !