Following up from a previous question of mine , where I asked how I could create a type that would model a unit (eg 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
, 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 -+|>
.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.