Hundreds of newtypes versus worse API

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 !

I don’t really know how to justify this, but I don’t think Second, Minute and Gram are types. Duration and Mass are types.

With the same justification as to why 1, 2, 3 are the same type. Second, Minute, etc are units, which measure values in a metric / type Duration, Mass, Length. You can always convert a value of one unit into another, as long as they share the same metric / type. I think this makes units more like constructors than values - a value being one of potentially many constructors.

In the same vein, we can lift a value 1, 2, 3 to the type level if we need to, and we can also have values that are measuring a metric in a specific unit - eg by like guaranteeing a value pattern matches against a specific constructor.

The point here is to statically verify the dimensionality of the conversion, so we need both units and dimensions to be at type level.

I don’t think I understand your suggestion. The library convert-units already has a core that guarantees that conversions are correct regarding dimensionality.

My question is more : what are the concrete consequences of having hundreds of newtypes + derving instance ? Is the cost big enough that one would prefer to have a single newtype that is tagged by some type ?

This is not specific to units libraries, but a more general dilemma I’ve been facing several times.

I guess the question would be : is it worth to use something like Data.Tagged or is it always better to have a bunch of newtypes that derive from Tagged ? With the newtypes, we always get a nicer API (better types, better error messages), but it is also messier (many newtypes instead of just one).

I would do the tagged approach of one newtype, many open type indexes.

Defining conversions is a fun thing to consider cuz where do they canonically live? the type class classic lol

i suppose one way is to do it like we do it in real life. SI units are the standard and everything expresses conversions to/from SI and the rest is easy.

Why do you need units to be at the type level to do that? That’s not clear to me.

Suppose you use conversion factors to convert to the base unit. For instance the conversion factor from minutes to second is factor = 60, from hours to seconds is factor = 3600, milli seconds to seconds is factor = 1 / 1000 and so on. Then to convert between two units, you only need to convert to the base unit (by multiplying factor of the first unit) and then to the other unit (by dividing by factor of the other unit).

To do so you need a type class to determine the conversion factor of each unit. So you need unit info at type level.

Otherwise you end up writing one conversion function for each pair of unit, which quickly becomes unmanageable.

Edit : by the way one nice trick is also to have the conversion factor at type level so that we make sure it is computed at compile time, to get no performance overhead. I still haven’t implemented this in convert-units but I plan to.

That makes sense. What’s your reason for choosing a single newtype over several newtypes ?

How would the pure newtype approach work with “non-atomic” units?

E.g. with Practice 2, if we have (meters 10) / (seconds 2), this could have type Qty (Meter / Second) (with some / type family or similar), but how would that work if Meter and Second are their own types?

Have you considered

newtype Qty u a = Qty a

data QMinute
data QSecond

type Minute = Qty QMinute
type Second = Qty QSecond

?

I don’t understand how that’s more type safe than having inSeconds :: Duration -> Double etc.. Can you elaborate?

For multypling and dividing units with the newtype approach, you can have a look here Data.Units.Core.System at the .*. and ./. constructors.

I copy here the code :

newtype ((u :: Unit) .*. (v :: Unit)) a = MulUnit a
  deriving ( Eq, Ord, Num, Fractional, Floating, Real, RealFrac, RealFloat, Functor)

newtype ((u :: Unit) .^. (n :: ZZ)) a = PowUnit a
  deriving ( Eq, Ord, Num, Fractional, Floating, Real, RealFrac, RealFloat, Functor)

So it is exactly the same as Qty (Meter / Second), except that you need to derive all the instances of Qty over and over again.

I think you have a design idea in mind that I did not think about, and completely different from what I do, so for now I don’t know what would be inside Duration. Is it stored in seconds ? In milli seconds ? In minutes ?

My point is, no matter the design you chose, you need to somehow know in what unit the quantity is stored.

The following is perfectly type safe (even though very inpractical with hundred of units)

newtype Second = Second Double
newtype Minute = Minute Double
newtype Meter = Meter Double
newtype Mile = Mile Double

secondToMinute  (Second a) = Minute (a / 60)
minuteToSecond  (Minute a) = Second (60 * a)
meterToMile ...
mileToMeter ...

But then again, my initial question arises. Should I use instead

newtype Qty u = Qty Double
data Second
data Minute
data Meter
data Mile

secondToMinute :: Rational a => Qty Second a -> Qty Minute a
secondToMinute  (Qty a) = Qty (a / 60)

minuteToSecond ::  Rational a => Qty Minute a -> Qty Second a
minuteToSecond  (Qty a) = Qty (60 * a)
meterToMile ...
mileToMeter ...

So my question is not specifically about units and is more generic : when there are hundreds of newtypes, each of which has more than ten deriving instances, are there some real draw backs on GHC’s side that justify a worse API using a single newtype like Qty?

I hope this clarifies what I’m asking, please tell me if I am not being clear :slight_smile:

Thank you for your suggestion. At first I thought it was great idea but there is a catch. As soon as you have compound units like Kilo Meters .*. Second .^. Neg 1 which corresponds to km / s, then having a distinction between QSecond and Second quickly makes thing less practical than using Qty directly.

That’s the beauty of abstract interfaces. You don’t have to know what would be inside duration. The interface is representation independent.

Who is “you”? The client of the interface does not need to know. They only need accessors which convert to whatever units they desire.

I’m sure it’s very type safe. I’m wondering if it’s more type safe to type units rather than to type dimensions.

But if I’m missing your point, feel free to ignore me.

I don’t think I understand what you’re proposing. It must be stored with some unit, I imagine that it is in seconds.

So you would have

newtype Duration = Duration Double 

durationToSeconds :: Duration -> Double
durationToSeconds = coerce

But now how do you convert a quantity in minutes to a quantity in seconds ?

I would imagine like this:

minutesToDuration :: Double -> Duration
minutesToDuration m = Duration (m * 60)

minutesToSeconds = durationToSeconds . minutesToDuration

Hm, I see. It feels like you might be able to make it work with type families that equate Qty x a .*. Qty y a with Qty (x .*. y) a and so on. But that’s extra complexity that users might pay for with confusing error messages or something.

Right, there will be some representation (equivalently, “implementation”), perhaps a Double representing a number of seconds, but that’s a choice that can be made without changing the interface, or the behaviour of the interface.

I don’t believe there’s such a thing as a “quantity in minutes”. There is such a thing as a duration (of type Duration, or perhaps Time) which can be measured in seconds (by inSeconds :: Duration -> Double) or minutes (by inMinutes :: Duration -> Double, say).

The interface must satisfy the property that inSeconds == (* 60) . inMinutes. So, I could define

-- Constructor not exposed
newtype Duration = DurationInSeconds Double

inSeconds (DurationInSeconds s) = s

inMinutes = (/ 60) . inSeconds

or equivalently

-- Constructor not exposed
newtype Duration = DurationInMinutes Double

inSeconds = (* 60) . inMinutes

inMinutes (DurationInMinutes m) = m

Does that make sense? (I’m not sure this is actually useful for what you’re trying to do, but it would be the first place I would look if I were trying to do calculations in a “unit safe” way.)