units-list: Extensible typed Dimensions

[ bsd3, library, math, physics ] [ Propose Tags ] [ Report a vulnerability ]

A package with statically typed dimensions that is more extensible than Dimensional and simpler than units Skip to the README for details.


[Skip to Readme]

Modules

  • Dimensions
    • Dimensions.CommonIsos
    • Dimensions.Data
    • Dimensions.DimensionalMisc
    • Dimensions.GetTermLevel
    • Dimensions.Match
    • Dimensions.Metric
    • Dimensions.Order
    • Dimensions.ParseMisc
    • Dimensions.Parser
    • Dimensions.Printer
    • Dimensions.TypeLevelInt
    • Dimensions.TypeMisc
    • Dimensions.Units

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1
Change log CHANGELOG.md
Dependencies base (>=4.21 && <4.22) [details]
License BSD-3-Clause
Copyright Ashok Kimmel 2025-2026
Author Ashok Kimmel
Maintainer ashok.kimmel@gmail.com
Category Math, Physics
Source repo head: git clone https://github.com/ashokkimmel/dimension/
Uploaded by AshokKim at 2025-11-23T17:09:11Z
Distributions
Downloads 0 total (0 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs not available [build log]
All reported builds failed as of 2025-11-23 [all 2 reports]

Readme for units-list-0.1

[back to package description]

units-list

A simple library oriented around providing easy and usable string-based units while still remaining extensible. Tries to be both simpler than the units package, and be more extensible than the dimensional package.

Example

worldPopulationInBillions = dimension "billion*people" 8.142
worldPopulation = fmap floor $ applyPos "billion" (*1e9) worldPopulationInBillions
daysInYear = dimension "days/years" 365
caloriePerDay = dimension "calories/days/people" 2000    
caloriesPerYear = caloriePerDay !* daysInYear !* worldPopulation 
> 5943659999270000 calories / years

Since everything has units, I chose Symbols to be the core dimensional type of this project.

Debug tip:

When getting an error like Could not match kind symbol with * in a polymorphic function like this assertSame :: Dimension n a -> Dimension n a -> Dimension n a, Try changing the type signature to assertSame :: forall k a (n :: [(k,Int')]). Dimension n a -> Dimension n a -> Dimension n a. This happened once, please report any other problems/solutions to ashok.kimmel@gmail.com, so I can add them to this README.

Implementation

A dimension is difined quite simply as:

type Dimension :: forall k. [(k,Int')] -> Type -> Type 
newtype Dimension a b = MkDimension b
    deriving stock (Eq,Ord,Functor)

There are two invariants: The Int' should never be zero, and the Dimension should be ordered. This means that there is only 1 valid type that satisfies the invariants per dimension.

k represents the kind used to index dimensions. One example is Symbols, another would be a Metric kind, another might be CaseInsenstiveStrings,etc. Int' represents the datatype used in this repository to represent Integers.

data Int' = Pos Nat | Neg Nat 

Neg n represents -(n+1). If you really need to create a dimension, you are encouraged to use ToNegInt and ToPosInt from Dimensions.TypeLevelInt, as implementation may change.

Creation

There are 8 ways to create dimensions

dim :: b -> forall a ->  Dimension (ValidParse @Symbol a) b 
dims ::Functor f => f b -> forall a ->  f (Dimension (ValidParse @Symbol a) b) 
dimension :: forall a -> forall b. b -> Dimension (ValidParse @Symbol a)  b
dimensions :: forall a -> forall f b. Functor f => f b -> f (Dimension (ValidParse @Symbol a) b)
dimensionPoly :: forall a -> forall b.  b -> Dimension (ValidParse a) b 
dimensionsPoly :: forall a -> forall f b. Functor f => f b -> f (Dimension (ValidParse a) b) 
dimNP :: forall a -> forall b. b -> Dimension (Format a) b
dimNPs :: forall a -> forall f b. Functor f => f b -> f (Dimension (Format a) b)

dim,dims,dimension and dimensions are the most common ones. They only work on symbols however, so if you want to use a different base, they would fail. dim and dims are like their longer counterparts just with the arguments flipped. dimensions is just dimension but lifted over a functor. Inspired by the ReadMe for the Dimensional library. The polymorphic versions suffer from type ambiguity as the kind of the resulting Dimension is unknown. It is reccomended that you add a wrapper if you plan to use a seperate dimension. dimNP is if you don't want to use the built in parser and want to manually specify the dimensions.

Note on parser

The parser is very simple: it checks for *,and /, splits them into sections, checks for ^ in the subsections and creates the dimensions accordingly. As a result, all of the following are valid

:k! Parse "*******" -> ['("", TI.Pos 1), '("", TI.Pos 1), '("", TI.Pos 1),'("", TI.Pos 1), '("", TI.Pos 1), '("", TI.Pos 1), '("", TI.Pos 1),'("", TI.Pos 1)]
:k! Parse "*/*/***^201" -> ['("", TI.Pos 1), '("", TI.Pos 1), '("", TI.Neg 0),'("", TI.Pos 1), '("", TI.Neg 0), '("", TI.Pos 1), '("", TI.Pos 1),'("", TI.Pos 201)]
:k! Parse "second/(meter*kilogram)"  -> [("kilogram),Pos 1),("(meter",Neg 0),("second",Pos 1)] 

dimension ensures that the invariants are upheld, but still allows annoyances. Check the types! Given the annoyance inherent to type level coding, this may not change.

Note on printer

The printer will not print the actual type as it is stored. e.g.

dimension "second/meter" 2 -> 2 second/meter

Despite the fact that the ordering of meter is actually before second, the printer tries to print the positive dimensions first. A useful note for debugging type errors.

Multiplying,dision,etc.

!+,!-,!*,!/,divD can be used for multiplying and dividing dimensions. They are mostly just specialized forms of liftD2, which works on two of the same and combineD2, which multiplies the two types. rt,rtn,!^,!^^ all allow for exponentiation and roots. They need type level arguments. rt,!^^ work on Int's, while rt and !^ work on Nats.
The type families !*,!/,RT,RTN all work at the type level and can be used with the NoParse functions to avoid parsing.

Transformations along dimensions

transform :: forall s t x a. TT.ToInt (LookupD0 s x) => (a -> a, a -> a) -> Dimension x a -> Dimension (Replace s t x) a 

This is used to completely switch a type parameter, whether it shows up in the positive or negative. Common usage would be with prefixes, kilogram to gram,etc.

transformPos :: forall s t x a. (TL.KnownNat (TI.ToNatural (LookupD0 s x))) => (a -> a) -> Dimension x a -> Dimension (Replace s t x) a

Like transform but only needs the switch in the positive direction. However, it requries that the dimension only occurs a positive number of times. Useful when you know that your unit occurs in the positive place.

apply :: forall x a. forall s -> TT.ToInt (LookupD0 s x) => (a -> a, a -> a) -> Dimension x a -> Dimension (Delete s x) a

Like transform, but consumes the dimension. Can be useful with things like billion, or mole.

applyPos :: forall x a. forall s -> (TL.KnownNat (TI.ToNatural (LookupD0 s x))) => (a -> a) -> Dimension x a -> Dimension (Delete s x) a

apply but only needs 1 function. I used this to eliminate the billion in the example.

same :: forall s t x. (forall a. Dimension x a -> Dimension (Replace s t x) a)

Assert that two things are the same, and replace one with another. Example: g gram grams all symbolize the same thing, but some places might use different ones.

mkisos :: forall y x a. Dimension x a -> Dimension (Isos y x) a

the same as repeated usage of same, uses a type level list.

inject :: (n -> n) -> forall a -> Dimension b n -> Dimension (a !* b) n

Allows you to add a dimension to a type, using a function. Example, adding a mole,

replace :: forall a -> Dimension b n -> Dimension (a !* b) n
replace = inject id

Replace can be used to replace simple things, even if you don't want to do it multiple times. Example: replace (Parse "billion/thousand^3")

Extracting dimensions

undimension, requires all tags be eliminated already.

getDimension allow you to specify the dimension, and getDimensionNP allows you to manually parse things.

Extending:

To extend this to a non-symbol base kind, define a ToDimension(for parsing), FromDimension (for printing), and Compare (for preserving invariants). Then you should probably define your own dim,dims,etc. functions and importing that module. The functionality should remain the same. You can also use the MatchAll class and the match function to define custom transformations along Symbols. Example: Get rid of all kilo prefixes in a dimension.