简体   繁体   中英

Modelling the composition of units (e.g. Inch, Dollar, etc) in Haskell

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 -+|> .


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.

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM