goldfirere / units

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

Understanding the use of Double #19

Closed spl closed 10 years ago

spl commented 10 years ago

I'm trying to understand how to get numbers into units. Currently, it seems like I must use Double for the quantities. But if I have some seconds in sec :: Int, and I want a Time, I'm doing fromIntegral sec % Second. Is that to be expected, or is there a better/different way?

goldfirere commented 10 years ago

The types you get by saying import Data.Metrology.SI indeed do use Double internally. You could theoretically use import Data.Metrology.SI.Poly to be able to specify your numerical type, but Int wouldn't work because it's not Fractional, as required by various functions in the library.

I don't have a great answer here because I'm not super-familiar with the numerical classes, etc. I will say that your desire is reasonable. I'll take a closer look at this when I get a chance to really update this library, which probably won't be for a month or so -- I'm facing down a deadline in my research at the moment.

If you (or others) see a better way to structure this, I'm all ears.

This is perhaps related to #6.

nushio3 commented 10 years ago

I think I can understand your usecase, and the problem here is that there are several ways of integer divisions. Haskell picks neither of them.
http://hackage.haskell.org/package/base-4.7.0.0/docs/Prelude.html#v:quot How about using a Rational , or Ratio Int?

Alternatively, we can define an orphan instance of Fractional Int, or create a newtype over Int that belongs to type class Fractional.

spl commented 10 years ago

Thanks, guys. (I realize this issue probably belongs on units-defs, so sorry for that. Oh well.)

Here's a concrete proposition prompted by your responses. To help me understand, can you give me a concrete description of what I should do?

Suppose I want my Time to be measured with a precision in attoseconds. How would I use Integer to represent the number of attoseconds in the Data.Metrology.SI units? (I'm guessing it starts with a newtype wrapper and Fractional instance, but I'd like to see more details.)

goldfirere commented 10 years ago

Here's a possible approach:

import qualified Data.Metrology.SI.Dims as D
import Data.Metrology

newtype AttoSeconds = MkAS Integer
  deriving (Num, Integral)

instance Fractional AttoSeconds where
  fromRational r = MkAS (numerator r `div` denominator r)
  (/) = div

-- assuming that you're only interested in storing times, not other dimensions
type AttoSecLCSU = MkLCSU '[(D.Time, Atto :@ Second)]

type Time = MkQu_DLN D.Time AttoSecLCSU AttoSeconds

I haven't tried to compile or run any of this, but it seems a workable approach. I do think it's possible to have better support for such idioms within units, but that will have to wait a month or more to see reality.

Does this seem to make sense? @nushio3 does this agree with your understanding of how this would work?

nushio3 commented 10 years ago

Dear @spl , now that I have a good problem (and time), let me extend @goldfirere 's workable approach.

nushio3 commented 10 years ago

Here's a working example. https://github.com/nushio3/practice/blob/master/units/attoparsec I prefer to create newtype for integer rather than attoparsec, because I'd like to multiple it by, at least, nondimensional quantities.

newtype FracInt = MkFI Integer
  deriving (Num, Integral, Real, Enum, Ord, Eq)

instance Show FracInt where
  show (MkFI x) = show x

instance Fractional FracInt where
  fromRational r = MkFI (numerator r `div` denominator r)
  (/) = div

-- snip --

main = do
  putStrLn "hello"
  print $ (mkAS 1234) |+| (mkAS 4567)
  print $ (mkAS 1) |/|  (MkFI 2 % Number)  
spl commented 10 years ago

@goldfirere @nushio3 Your examples make it clear what is involved. Thanks.

Just to add a concrete example from something I was looking into...

I'm working with a web app that stores time configuration parameters in an external YAML file. YAML, like JSON, only supports basic types like numbers and strings. We treat numbers in certain fields as integers representing seconds, intended for delaying threads.

Currently, I think we keep these seconds values as Int. That's not ideal for a type that should indicate more about the meaning of the number. But should we create a type SecondsDelay (for example), or should we use the Time from units-defs? Since the value is external to the program, I lean towards the former, which includes the units in the type.

Now, assuming we go with SecondsDelay, how do we use it with threadDelay? At first, I was considering the time-units. The package is simple, intuitive, and seemed suitable. To use threadDelay, I can use Second (from time-units) instead of SecondsDelay and convert to microseconds:

let n :: Second = ...
threadDelay (toMicroseconds n)

(Alternatively, I could use a handwritten threadDelaySeconds :: Second -> IO ().)

On the other hand, to use your example, my SecondsDelay would be defined like FracInt. But it seems like I have to recreate the universe (i.e. the LCSU) that you created in units-defs.

Whereas, if I changed my mind and used Time as provided in units-defs for the type of the number in the YAML file, I would not need to recreate anything and could use a Double as seconds with less-than-a-second precision in the config file. Is that correct?

(BTW, I don't really have a goal with this issue. Just working out how to use units in general and in my particular code.)

goldfirere commented 10 years ago

@spl says:

On the other hand, to use your example, my SecondsDelay would be defined like FracInt. But it seems like I have to recreate the universe (i.e. the LCSU) that you created in units-defs.

The LCSU that units-defs prepares for you is meant to be suitable for most applications. We were thinking of physics-oriented scenarios, where you typically want floating-point precision. Your scenario is different, and creating your own LCSU is suitable, and in fact shows off the power of the ability to define an LCSU. I think this is appropriate here.

Whereas, if I changed my mind and used Time as provided in units-defs for the type of the number in the YAML file, I would not need to recreate anything and could use a Double as seconds with less-than-a-second precision in the config file. Is that correct?

But, you would be left with conversions among numeric types. If that is acceptable, this is perhaps the easiest approach with the current design of units.

I hope I've captured what you're looking for here...

spl commented 10 years ago

But, you would be left with conversions among numeric types.

What I meant to say is that I would use a Double from the “beginning,” i.e. from configuration, for Time so that I wouldn't use any conversion.

Otherwise, yes, you summed it up well.

mtolly commented 10 years ago

To store time with an attosecond precision, you could use Data.Fixed which uses Integers to store fractional quantities using a chosen resolution:

{-# LANGUAGE DataKinds #-}
module Atto where

import Data.Fixed
import Data.Metrology
import Data.Metrology.SI
import Data.Metrology.Show ()
import qualified Data.Metrology.SI.Dims as D

data E18 = E18

instance HasResolution E18 where
  resolution _ = 10 ^ 18

type MkQu_Atto dim = MkQu_DLN dim DefaultLCSU (Fixed E18)
type AttoTime = MkQu_Atto D.Time

fromAttoseconds :: Integer -> AttoTime
fromAttoseconds atto = MkFixed atto %% Second

fromSeconds :: Rational -> AttoTime
fromSeconds secs = realToFrac secs %% Second

toAttoseconds :: AttoTime -> Integer
toAttoseconds time = case time ## Second of
  MkFixed atto -> atto
goldfirere commented 10 years ago

@mtolly 's comment looks like a great solution to me; I don't see a way of improving on that. I'm closing this ticket, but do re-open if you see a way for further improvement.