Closed spl closed 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.
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
.
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.)
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?
Dear @spl , now that I have a good problem (and time), let me extend @goldfirere 's workable approach.
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)
@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.)
@spl says:
On the other hand, to use your example, my
SecondsDelay
would be defined likeFracInt
. But it seems like I have to recreate the universe (i.e. theLCSU
) that you created inunits-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 inunits-defs
for the type of the number in the YAML file, I would not need to recreate anything and could use aDouble
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...
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.
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
@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.
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 insec :: Int
, and I want aTime
, I'm doingfromIntegral sec % Second
. Is that to be expected, or is there a better/different way?