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?
First, let me give this opinion: IMO the type should be called
Length, notInch. The constructor should be calledInches, but the beauty of representing physical quantities with types is that the unit really becomes an “invisible” implementation detail. You could in fact use-XPatternSynonymsto 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(unlikeNum) 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/sby a functionTime -> 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 -> Lengthis 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 functionTime -> 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-XGeneralizedNewtypeDerivingfor making instances of thevector-spaceclasses, then there's a Template Haskell macro for also defining the linear-map etc. types.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.It's not really clear to me what you want the
priceAreaRatiofunction 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.