goldfirere / units

The home of the units Haskell package
94 stars 19 forks source link

Does it make sense to have a polymorphic "NumberOf" dimension? #60

Closed ocharles closed 4 years ago

ocharles commented 4 years ago

Apologies for the somewhat sloppy issue title, it's hard to summarise this in a single sentence!

I'm using units to build a pricing model for some work we do at CircuitHub. Namely, I'm interested in a pricing model grounded by a sensible real-world interpretation. units works nicely for this - I introduced a new Cost dimension (with $ unit), and now I can say things like MkQu_DLN (Cost :/ Area) - nice!

However, a lot of our costs are actually per some specific unit. For example, parts prices expressed as unit prices. Currently, these are MkQu_DLN Cost, but they are really cost per something - and here my modeling is a bit stuck.

I could introduce a new PartQuantity dimension, but this idea of a count seems to keep cropping up. It seems natural to me to have a

data NumberOf x

dimension, such that I can have things like partUnitCost :: MkQu_DLN (Cost :/ NumberOf Part).

I'm curious if this really fits in with the spirit of units though. In particular, it seems like a strange dimension, as it doesn't really have any units, other than an exact count of the things in question. On the other hand, it does a very good job of making sure I scale a per-unit part cost by the actual number of parts I need to order!

What do the others think?

goldfirere commented 4 years ago

At first, I thought you were describing Number. But now I see you want to distinguish NumberOf Widget from NumberOf Wodget. That sounds entirely sensible to me. Is it definable outside of units? It's been a while -- I'm not sure off the top of my head. If not, I, for one, would be willing to incorporate this idea.

The existing Number could just become NumberOf (). Which, when you say it aloud ("number of unit"), makes perfect sense. :)

ocharles commented 4 years ago

Aha, nice, I didn't see Number. Yes, this should all be definable outside this library. I'll have a play and see what I can come up with. And thank you for the very prompt reply!

ocharles commented 4 years ago

Ok, the only thing that I think Number doesn't do is act as a Dimension, which prohibits it from being composed with another dimension. So currently you can't say MkQu_DLN (Area :/ NumberOf Widget), for example.

Take this ghci session. I know that two widgets take up 5m^2, and I want to calculate the area for 10 widgets.

> :set -XDataKinds
> :set -XTypeOperators
> import Data.Metrology
> import Data.Dimensions.SI (Area)
> import Data.Units.SI (Meter(Meter))
> let areaPerWidget = (5 % (Meter :* Meter)) |/| (2 % Number)
> let tenWidgets = 10 % Number
> areaPerWidget |*| tenWidgets
25.0 m^2

Ok, fine, ten widgets will require 25 m^2.

But I can also write...

> let twelveWodgets = 12 % Number
> areaPerWidget |*| twelveWodgets
30.0 m^2

But this is meaningless - it should be a type error, but there's no information in areaPerWidget that would even allow us to create a type error:

> areaPerWidget
2.5 m^2

> :t areaPerWidget
areaPerWidget
  :: Fractional n =>
     Qu '[ 'F Data.Dimensions.SI.Length ('S ('S 'Zero))] 'DefaultLCSU n

The problem is Number is dimensionless, but we do want a dimension. Really, the dimension of areaPerWidget should be (Area :/ NumberOf Widget), rather than just Area.

But it seems like the whole point of Number is to be dimensionless, so I'm not sure about adding a dimension instance to it.

TL;DR: If we have NumberOf it seems that it wants to be a dimension as well, but does this make sense?

goldfirere commented 4 years ago

You're right -- I had confused unit and dimension in my thinking. You want NumberOf to be a dimension. (Actually, a family of dimensions.) But I think nothing stops you from doing this on your own. (That's one of the beauties of units. It's fully customizable.) The fact that a "number of things" doesn't correspond to a dimension in physics shouldn't stop you.

This does mean that Number, in units, is not what you want.

Or am I missing something more important?

ocharles commented 4 years ago

No, you're precisely right, I just wanted to see if this is something that actually made sense in the context of the library. I'm working with:

{-# language DataKinds #-}
{-# language PolyKinds #-}
{-# language TypeFamilies #-}

module Data.Metrology.NumberOf ( NumberOf( NumberOf ) ) where

-- units
import Data.Metrology

data NumberOf x =
  NumberOf
  deriving
    ( Show )

instance Dimension ( NumberOf x )

instance Unit ( NumberOf x ) where
  type BaseUnit ( NumberOf x ) =
    Canonical

  type DimOfUnit ( NumberOf x ) =
    NumberOf x

type instance DefaultUnitOfDim ( NumberOf x ) =
  NumberOf x

and having success. Happy to contribute that back into units if you feel it's generally useful, or we can keep it in house.

goldfirere commented 4 years ago

I believe others have used the same type as a dimension and as a unit, but that's not technically a supported configuration.

I'm eager to keep units as agnostic as possible to actual units and dimensions. Perhaps contribute it to units-defs if you like.

Glad it's working for you!

ocharles commented 4 years ago

Great! I'll play around a bit more and make sure it's actually usable. If so, I may contribute it back upstream. Thanks again for the fun library!