Following up from a previous question of mine, where I asked how I could create a type that would model a unit (e.g. Inch
) as a type in Haskell, I now face the problem of how to perform operations on that and other units and mix them correctly.
For instance, given:
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
import GHC.Generics
import Data.VectorSpace
newtype Inch = Inch Double
deriving (Generic, Show, AdditiveGroup, VectorSpace)
How can I define a function to compute the area with the following signature?
circleArea :: Inch -> SquareInch
And how about the "price area ratio" (say, dollars per inch^2)?
priceAreaRatio :: Inch -> Price -> PricePerSquareInch
Those signatures seem wrong: how can I express that SquareInch
is actually Inch * Inch
? And that the PricePerSquareInch
really is Price / (Inch * Inch)
?
I have found a potential solution here but I am not well-versed in Haskell enough to understand whether that is just a toy solution, an experiment, or really a good practice.
How can I model my problem?
CodePudding user response:
First, let me give this opinion: IMO the type should be called Length
, not Inch
. The constructor should be called Inches
, but the beauty of representing physical quantities with types is that the unit really becomes an “invisible” implementation detail. You could in fact use -XPatternSynonyms
to work with different constructors for different units of the same length type, and/or lens-isomorphism for unit conversion. But this is somewhat tangential to the question.
The comments have already linked to existing physical-units libraries. Using one of those would definitely be the most sensible approach for a real project.
Stephan Boyer's blog post you've linked is definitely intriguing, however representing dimension-quotiens by general functions is really not very practical.
I'll show something in between: still using bespoke types instead of bell&whistley physical-unit ones, but anyway within a more numerics-suitable framework.
Already in your previous question, I pointed to the vector-space library, because VectorSpace
(unlike Num
) is a suitable abstraction for physical quantities. As you've noticed, it only supports addition and scaling-by-real-number though, not multiplying or dividing physical quantities.
But the mathematical concept of vector spaces does extend to such operations as well. The Boyer blog goes in this direction: it represents m/s
by a function Time -> Length
. Which does make sense: what is a velocity? It's something that tells you, “if you wait for so and so long, how long will the object travel”.
However, Time -> Length
is a way too big type, both in the sense that storing an arbitrary function is total overkill and inefficient for something that you know can also be represented by a single number, but more importantly also in that it doesn't capture the fundamental idea: a velocity is by definition a linearized function Time -> Length
, because for sufficiently small time-deltas the motion can always be approximated by the first two Taylor terms.
And it is well known that linear functions are sensibly described as matrices†. In our case, both time and length is a 1-dimensional space, so it'll be a 1×1 matrix... IOW a single number again.
This idea of abstracting over linear functions in a type-safe manner but still having numbers/matrices as the internal representation is what I wrote the linearmap-category package for. It builds upon vector-space
, but the classes turn out to become a lot uglier. Fortunately, for a simple type like yours the instances can be auto-generated: first you use -XGeneralizedNewtypeDeriving
for making instances of the vector-space
classes, then there's a Template Haskell macro for also defining the linear-map etc. types.
{-# LANGUAGE TemplateHaskell, UndecidableInstances, GeneralizedNewtypeDeriving #-}
import Math.LinearMap.Category
import Math.LinearMap.Category.Instances.Deriving
import Data.VectorSpace
import Data.Basis
newtype Length = Inches Double deriving (Show, AdditiveGroup, VectorSpace, HasBasis)
makeLinearSpaceFromBasis [t| Length |]
newtype Price = Euros Double deriving (Show, AdditiveGroup, VectorSpace, HasBasis)
makeLinearSpaceFromBasis [t| Price |]
Now you can use the type combinators from linearmap-category
, and always immediately have the vector space operations on them! As I already said, quotients correspond to linear maps. Products correspond to tensor products, which again is for the 1-dimensional case also just a newtype wrapper around a single number.
type Area = Length ⊗ Length
type PricePerArea = Area > Price
circleArea :: Length -> Area
circleArea r = (4*pi)*^(r⊗r)
It's not really clear to me what you want the priceAreaRatio
function to do, but this would probably be implemented with - |>
.
†Actually though, matrices are also often not a good representation, because they scale quadratically in the dimension of the spaces. Of course this is not relevant here.