packages feed

inch-0.1.0: examples/Units.hs

{-# OPTIONS_GHC -F -pgmF inch #-}
{-# LANGUAGE RankNTypes, GADTs, KindSignatures, ScopedTypeVariables,
             NPlusKPatterns #-}

{-
  An example of the need for type-level *integers* as well as natural
  numbers: representing units of measure. Quantites can only be added
  if the units match, and multiplication and division change the units
  appropriately. There is no runtime representation of units, and
  hence no runtime cost (at least there wouldn't be if Quantity was a
  newtype).

  See Bjorn Buckwalter's dimensional package
  (http://dimensional.googlecode.com/) for a more comprehensive
  implementation of this idea, using existing features of GHC Haskell.
-}

module Units (Quantity, dimensionless, metres, seconds, kilograms,
                 minutes, hours, plus, minus, inv, times, over, scale,
                 kilo, centi, units) where


-- Unit collects indices for the powers of metres, seconds and grams
-- (other units are omitted for simplicity). Quantity has a phantom
-- type parameter which will usually be instantiated with some units,
-- but this allows us to write functions that are completely
-- polymorphic in the units very easily. Note that the Q constructor
-- should not be exported!

data Unit :: Integer -> Integer -> Integer -> * where

data Quantity :: * -> * -> * where
    Q :: forall a u . a -> Quantity u a
  deriving Show


type Dimensionless     = Unit 0 0 0
type Metres            = Unit 1 0 0
type Seconds           = Unit 0 1 0
type Kilograms         = Unit 0 0 1
type MetresPerSecond   = Unit 1 (-1) 0
type Newtons           = Unit 1 (-2) 1


-- Define some basic constructors

dimensionless :: a -> Quantity Dimensionless a
dimensionless = Q

metres :: a -> Quantity Metres a
metres = Q

seconds :: a -> Quantity Seconds a
seconds = Q

kilograms :: a -> Quantity Kilograms a
kilograms = Q

minutes = (.) (scale 60) seconds
hours   = (.) (scale 60) minutes


-- Arithmetic of units

plus :: Num a => Quantity u a -> Quantity u a -> Quantity u a
plus (Q x) (Q y) = Q (x + y)

minus :: Num a => Quantity u a -> Quantity u a -> Quantity u a
minus (Q x) (Q y) = Q (x - y)

inv :: forall (m s g :: Integer) a . Fractional a => 
           Quantity (Unit m s g) a -> Quantity (Unit (-m) (-s) (-g)) a
inv (Q x) = Q (recip x)

times :: forall (m s g m' s' g' :: Integer) a . Num a => 
             Quantity (Unit m s g) a -> Quantity (Unit m' s' g') a ->
                 Quantity (Unit (m + m') (s + s') (g + g')) a
times (Q x) (Q y) = Q (x * y)

over x y = times x (inv y)

scale :: Num a => a -> Quantity u a -> Quantity u a
scale x (Q y) = Q (x * y)

pow :: forall (m s g :: Integer) a . Fractional a =>
           pi (k :: Nat) . Quantity (Unit m s g) a ->
               Quantity (Unit (k * m) (k * s) (k * g)) a
pow {k} (Q x) = Q ((^^) x k)

sqr = pow {2}


-- We can write unit prefixes as transformers of the constructors...

type Prefix u a = (a -> Quantity u a) -> a -> Quantity u a

prefix :: Num a => a -> Prefix u a
prefix n f x = scale n (f x)

kilo = prefix 1000
centi = prefix (recip 100)
milli = prefix (recip 1000)

-- ...allowing things like this:

km  = kilo metres
cm  = centi metres
mm  = milli metres
g   = milli kilograms


-- With a special name for flipped application, we can write
--     units 3 cm                                    for  0.03 m
--     units 15 (kilo metres) `over` units 3 hours   for  1.39 m/s

units :: a -> (a -> Quantity u b) -> Quantity u b
units x f = f x



-- distanceTravelled :: (Num a, Fractional a) => Quantity Seconds a -> Quantity Metres a
-- or we can just omit the type annotations, and get good inference behaviour
distanceTravelled t = plus (times vel t) (times accel (sqr t))
  where
    vel    = over (units 2 metres) (units 1 seconds)
    accel  = over (units 36 metres) (sqr (units 10 seconds))


-- This is Kennedy's example of a function whose type cannot be
-- inferred by the units-of-measure type system in F#, because of
-- difficulties with generalisation (see Kennedy, Types for
-- Units-of-Measure: Theory and Practice, 2009, section 3.10).

nastyExample = \ x -> let d = div x
                      in (d mass, d time)
  where
    div = over
    mass = units 5 kilograms
    time = units 3 seconds