3

With the following types

newtype Kms =
  Kms Double
newtype Hours =
  Hours Double
newtype KmsPerHour =
  KmsPerHour Double

I would like to have the following

foo :: KmsPerHour -> Hours -> Kms
foo kmsPerHour hours = kmsPerHour * hours

Is this possible? In a perfect world the solution would support (/) as well and for several different units (m, m/s, m/s/s for example)

That way I can more easily make sure that all the units I use match up and that the calculations is correct. The way I do it now (but with several more types of units in different combinations) is

foo :: KmsPerHour -> Hours -> Kms
foo (KmsPerHour kmsPerHour) (Hours hours) = Kms $ kmsPerHour * hours

I checked this Can you overload + in haskell? and https://hackage.haskell.org/package/alg-0.2.10.0/docs/src/Algebra.html#%2B but those are just a->a->a

Update

My try looks like this. I really like how it very tightly secures the types and it supports nested types and I do not need to define every type combination but I do not know if this is a good way to go - especially since the type family option seems elegant.

Ofc, the functions below can be changed to operators

class Unit a where
  unwrap :: a -> Double
  unitMap :: (Double -> Double) -> a -> a

instance Unit Kms where
  unwrap (Kms x) = x
  unitMap f (Kms x) = Kms $ f x

newtype Per a b =
  Per a
  deriving (Eq, Show)

instance (Unit a, Unit b) => Unit (Per a b) where
  unwrap (Per x) = unwrap x
  unitMap f (Per x) = Per $ unitMap f x

multWithUnits :: (Unit a, Unit b) => Per a b -> b -> a
multWithUnits (Per x) z =
  let zVal :: Double
      zVal = unwrap z
   in unitMap (* zVal) x

divWithUnits :: (Unit a, Unit b) => a -> b -> Per a b
divWithUnits x y =
  let yVal = unwrap y
   in Per (unitMap (/ yVal) x)

multUnitWith :: (Unit a, Unit b) => Double -> Per a b -> Per a b
multUnitWith factor = convert (* factor)

divUnitWith :: (Unit a, Unit b) => Double -> Per a b -> Per a b
divUnitWith factor = convert (/ factor)

toKmsPerHour :: Kms -> Hours -> Per Kms Hours
toKmsPerHour kms h = km `divWithUnits` h

distance :: Per Kms Hours -> Hours -> Kms
distance speed time = speed `multWithUnits` time

I ommited implementations of Hours, and the instances of Num, Ord and other things to not bloat the post.

addKms :: Kms -> Kms -> Kms
addKms k1 k2 = k1 + k2

Thoughts?

Community
  • 1
  • 1
J. Doe
  • 33
  • 3
  • 4
    Short answer is no, because this would violate the expected closure of a `Num` instance. Longer answer is to checkout the [`units` library](https://github.com/goldfirere/units/blob/master/Data/Metrology/Combinators.hs) which uses the `:/`, `:*`, `:+` etc. – Dair Aug 06 '19 at 21:58
  • 1
    You might also want to look at the "dimensional" library http://hackage.haskell.org/package/dimensional Warning: advanced type-fu required to understand what is going on here. – Paul Johnson Aug 07 '19 at 15:10
  • @PaulJohnson, how do `units` and `dimensional` compare? – dfeuer Aug 07 '19 at 18:08

2 Answers2

1

No, giving the Prelude's multiplication operator that type is not possible. But you can make your own operator and give it whatever type you want. You can even name it (*) if you want...

import Prelude hiding ((*))
import qualified Prelude
KmsPerHour a * Hours b = Kms (a Prelude.* b)
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
1

a -> b -> c by itself is clearly a much too weak signature, as it would also allow stuff like

  (1`KmPerHour`) * (1`KmPerHour`) :: Hours

You do need some restriction on the types. There are broadly two options for that:

  • a multi-param type class with fundeps.

    {-# LANGUAGE FunctionalDependencies #-}
    
    infixl 7 ·
    class PhysQttyMul a b c | a b -> c, a c -> b, b c -> a where
      (·) :: a -> b -> c
    
    newtype Length = Kms {getLengthInKm :: Double} -- deriving Data.VectorSpace.VectorSpace
    newtype Time = Hours {getTimeInHours :: Double}
    newtype Speed = KmPerHour {getSpeedInkmph ::Double}
    
    instance PhysQttyMul Speed Time Length where
      KmPerHour v · Hours t = Kms $ v*t
    instance PhysQttyMul Time Speed Length where
      Hours v · KmPerHour t = Kms $ v*t
    
  • a type family that just calculates the type of the product of two operands at compile-time.

    type family PhysQttyProd a b :: *
    
    type instance PhysQttyProd Speed Time = Length
    type instance PhysQttyProd Time Speed = Length
    

    ...and then you still need a typeclass for the actual value-multiplication

    class PhysQttyMul a b where
      (·) :: a -> b -> PhysQttyProd a b
    
    instance PhysQttyMul Speed Time where
      KmPerHour v · Hours t = Kms $ v*t
    instance PhysQttyMul Time Speed where
      Hours v · KmPerHour t = Kms $ v*t
    

The latter option looks much complicated, but it has some practical advantages when actually defining your unit system. In particular, it lends itself well to a generic type that can express any physical quantity, via essentially the exponents of a set of base units. That's how the units library does it.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • This looks elegant, I updated my question with my first take on it. I do not have enough knowledge to evaluate the difference between that and a type family solution. Any thoughts are welcome. – J. Doe Aug 07 '19 at 11:55
  • 2
    @J.Doe I think you ought to revert your edit to the question and post it as a new, follow-up question. otherwise it gets too confusing to follow. Moreover, if your code is working, i.e. you don't have a problem with it and instead ask for a discussion and "thoughts", i.e. a *review*, then the right place to post it might be on CodeReview stackexchange. – Will Ness Aug 07 '19 at 12:29
  • I understand now that my comment and my edit could be misconstrued and made possible the question worse. What I tried to convey was that I potentially have a solution but I cannot see the long term differences between that and the type family one. So maybe I should add it as an answer and people can vote it down if it is not the best solution? – J. Doe Aug 07 '19 at 16:04
  • @J.Doe if you aren't sure in its correctness, it is *not* an answer, so why post it? if you have a question, *ask*. :) – Will Ness Aug 07 '19 at 18:30