Elm-units 1.0: Units handling for Elm

After a while in the oven, I’ve just published ianmackenzie/elm-units 1.0! From the README:

elm-units is useful if you want to store, pass around, convert between, compare, or do arithmetic on:

  • Durations (seconds, milliseconds, hours…)
  • Angles (degrees, radians, turns…)
  • Lengths (meters, feet, inches, miles, light years…)
  • Temperatures (Celsius, Fahrenheit, kelvins)
  • Pixels (whole or partial)
  • Speeds (pixels per second, miles per hour…) or any other rate of change
  • Any of the other built-in quantity types: areas, accelerations, masses, forces, pressures, currents, voltages…
  • Or even values in your own custom units, such as ‘number of tiles’ in a tile-based game

It is aimed especially at engineering/scientific/technical applications but is designed to be generic enough to work well for other fields such as games and finance.

Very briefly, elm-units lets you replace raw Float or Int values with type-safe Length, Duration, Angle etc. values, helping avoid units mismatches (seconds vs milliseconds, degrees vs radians etc.) without sacrificing much in the way of flexibility or efficiency. Check out the README for a more thorough introduction!

It should already be stable and featureful enough to start using in Elm apps, particularly those with a scientific/engineering focus. I’d like to spend a while gathering feedback on the API, which may lead to some breaking changes and a 2.0 release, but after that I’m hoping to keep the API extremely stable (likely only have major version changes when Elm itself changes). My hope is that once this happens, other packages will be able to confidently use the elm-units types as part of their public APIs - I’m really excited about the possibilities that would come from having a standard, type-safe, “units-safe” way of passing around Lengths, Durations, Angles etc. between functions in different packages (where appropriate - in many cases keeping things simple with just a Float will be the right choice).

Big thanks to all those who helped with initial development of elm-units:

(Sincere apologies to anyone I’ve missed.)

Finally, although I think the most fundamental unit/quantity types are already in place, there will certainly be more added over time - I have a list of ones I’d like to add in issue #6, and in fact there’s already a pull request underway to add a Volume module. Let me know if there are others you would find useful, and feel free to open a pull request if you’re willing to take a stab at implementing one of them yourself! (Although please leave a comment on the issue before you do, so we can avoid duplicate work.)

Happy to answer questions, take feedback, field requests for new functionality etc. =)

28 Likes

Great project! A question about your desired project scope: I’ve been wanting to implement different functions for geographical distance in Elm. Would it make sense to include operations like these in the Length module in elm-units, or would a separate library (using elm-units) be a better fit?

Thanks @runarfu! I think that’s a great example of something that would work well as a separate package that used elm-units; I’d like to keep elm-units itself very focused/limited so that the API can be kept extremely stable. I think in many cases adding more units types to elm-units makes sense (for example, it’s helpful that the Length type is defined in elm-units itself, to guarantee that everyone using elm-units is using the same one), but I think functionality like geographical distance, physics equations (kinematics/dynamics/thermodynamics) etc. would fit better in separate packages so their APIs can evolve independently.

So if there are reasonably general-purpose units types that would be needed by an elm-geographical-distance package that aren’t currently in elm-units, then I’d be open to adding them, but if everything can be done using Length and Angle values etc. then I think it makes sense to just implement those functions in a separate package. I think it would be cool to see some elm-kinematics, elm-thermodynamics etc. packages cropping up too.

I think there’s an interesting analogy to SVG - the core elm/svg package is pretty no-frills, and so you have things like typed-svg and my own elm-geometry-svg that build on top of it with higher-level functionality. However, since both typed-svg and elm-geometry-svg just produce values of the Svg and Attribute types defined in elm/svg, you can freely combine all three! For example, you could create an SVG shape element using elm-geometry-svg, add attributes to it using functions from typed-svg, and then add the element to a plain Svg.g group.

4 Likes

Great work @ianmackenzie!

This API looks awesome :sunglasses:

1 Like

This is a well-thought-out package with lots of rich detail. I really like the API for Quantity types, which seems vary polished. I wonder about the decision to represent physical quantities as a single underlying unit, though.

The documentation says “the choice of internal units can mostly be treated as an implementation detail,” but I think it should point out how the internal unit conversion interacts with floating point precision. What I mean is the fact that Temperature.degreesFahrenheit 10 |> Temperature.inDegreesFahrenheit |> String.fromFloat yields 9.999… That this will happen is fairly obvious to people who know about floating point math issues, but it might surprise users in e.g. an educational context.

Following from the above, I thought it would be nice for presentation purposes to have a function that rounds a Quantity to a given number of decimal places (like the Number.toFixed function in javascript). The Quantity library doesn’t have this AFAICT, but then again neither does Elm for vanilla Floats. Maybe the existence of this library could help establish the use case for such a function in the language core.

More broadly, there are situations where representing all lengths internally as meters makes sense, and others where it would rather make sense for the internal representation to be in feet. I can imagine an API that looks like this:

type Length = Feet Float | Meters Float

meters : Float -> Length
meters = Meters

feet : Float -> Length
feet = Feet

toFeet : Length -> Float
toFeet l =
    case l of
        Feet n -> n
        Meters n -> n * 0.3048

-- toMeters...
        
plus : Length -> Length -> Length
plus l1 l2 =
    case (l1, l2) of
        (Feet a, Feet b) -> Feet <| a + b
        (Meters a, Meters b) -> Meters <| a + b
        _ -> Feet <| (toFeet l1) + (toFeet l2)

-- minus, times, etc.

Conversion is only done when combining unlike units; calculations whose inputs are the same units will never undergo conversion. (We do still need to stipulate what the basic representation is of the combination of unlike units: I’ve chosen feet here, but that’s arbitrary.) The drawback of this approach is that Length.plus and Duration.plus (and all the other arithmetic functions) can no longer share an implementation. There are tradeoffs to be made between simplicity of implementation (reusing generic Quantity functions for all arithmetic) and mathematical correctness (avoiding unnecessary imprecision-inducing unit conversions). I am sure you have thought about these issues, and it would be nice if the documentation mentioned them.

Again, I think this is a very good package in total and I don’t want the nature of these critiques to detract from that overall impression!

PS I think there’s also something that could be said in principle about avoiding floating point arithmetic entirely for integer quantities, and picking a basic representation that facilitates that: e.g. in the imperial system, using inches rather than feet as a basic unit allows you to avoid floats more often. But given (a) Elm’s type system and (b) the fact that all JS numbers are floats under the hood anyway, that discussion goes farther than necessary. But it would be something to revisit if Elm ever gets e.g. a WebAsm backend, since (I think) that platform does have proper ints.

1 Like

Thanks for the thoughtful feedback @Aaron_Ecay! I agree that the roundoff induced by having a single internal representation is slightly unfortunate, but I still think it’s the best overall approach for a few reasons.

First and most importantly is efficiency - storing values as a union type internally would mean some pattern-matching overhead, but more importantly it would mean that every Length etc. value would be internally represented as a JavaScript object and would require dynamic memory allocation. With the current design, when elm make --optimize is used, every Quantity value will be stored as just a plain JavaScript number, which is far more efficient - and at least personally I’m interested in using these values for high-performance numeric/scientific/technical code.

As you mention, this would also mean you couldn’t have a shared Quantity module and would need to replicate all the various functions in each module (similar to what had to be done for Temperature).

More fundamentally, at least with current CPU architectures, I think roundoff is kind of just a fact of life that you will have to deal with when working with floating-point values - you can use techniques like you’ve described to avoid roundoff in a couple specific cases, but you’re still going to run into stuff like 0.1 + 0.2 /= 0.3. I’d almost prefer to have people hit it right away and learn how to deal with it instead of expecting things to be exact and going down a path that’s going to get them in trouble later.

I do think functions for rounding to a specific number of decimals places are useful, but I see that as part of a Float -> String function (an output formatting problem), not a Float -> Float function. Are there use cases for ‘round to three decimal places’ that don’t also involve converting to a string?

As a side note, the package already does support Int-valued quantities which do then use exact arithmetic - which I think could be useful with things like Quantity Int Pixels to restrict values to a whole number of pixels, or Quantity Int Cents to do exact math on currency values (with ways to convert back and forth between Int and Float quantities, of course!).

Certainly happy to add a bit of clarification to the docs instead of just sweeping rounding issues under the rug with a broad “internal implementation doesn’t matter” comment - I’ll think about the best way to word that.

1 Like

Hmm. I’m mostly thinking about this from the perspective of “front end” app development, like quantities in a recipe, times on a scheduling app, simple calculators for online shopping (if my fingers are x long and my hands are y wide, what size gloves do I buy?). All domains where presenting simple round numbers is more important than retaining lots of accuracy or speed. So I think that explains some of the differences in our perspective.

In that vein, I think the example of temperature is illuminating: it’s one example of where having a domain specific model of how quantities/units behave differently than mathematical numbers helps, and (I’d say) it’s just one instance of that situation among many. On the other hand though I can also see how there’s a domain (“science,” for lack of a better word) where lots of different units (barring temperature) do behave more or less like mathematical numbers, and it sounds like that’s what you’re targeting.

Nope, string conversion is what I had in mind.

I’m impressed by the really good examples in the documentation! Writing examples is hard, but you’ve nailed it.

Random question: Does it support volumes? Not that I need them, I’m just trying to understand how things work.

1 Like

Funny you should ask @lydell! There is a pull request underway right now to add a Volume module =) So that should be included in the next minor release. That said, it’s also pretty easy to define your own units (that still work with all the Quantity functions) if there’s one missing from elm-units itself. See here for one example of defining custom units, but frankly any of the existing types (Length, Duration etc.) could also be used as an example - none of them use any hidden/non-exposed functionality or anything, so they could just as easily have been implemented in a third-party package or directly inside an app.

1 Like

I certainly realize I’m coming to this with a particular set of biases/perspectives! And I do want to make the package as generally useful as possible - I just think that solving the ‘round numbers’ issue with nice string formatting packages like elm-format-number is a more universal solution than carefully avoiding internal roundoff (it will work even if you start doing addition, subtraction etc.) and it allows the Quantity representation to stay extremely efficient for use in games, simulations, analysis, visualization etc. (I don’t think I’m the only one for whom efficiency will be an important concern!)

I’d love to hear of examples of other quantity types that don’t behave like mathematical numbers - that was one of the questions I brought up on Slack during the early design phases, but all anybody could think of at the time was temperature.

Just realized I did forget to thank one person! I used @dmy’s elm-doc-preview tool extensively when writing the docs for elm-units. It’s been fantastic - super fast and frictionless and (as far as I can tell) matches how the final documentation will look on the Elm package site pretty much exactly. Makes writing documentation a much more pleasant experience!

3 Likes

Quick question: How difficult would it be (would it be possible at all without typeclass-like magic?) to make elm-units work with other types of numbers?

Most importantly, arbitrary-precision decimals: The current documentation states that the library might be used for finance, but please refrain from using Floats for representing monetary amounts. Even if you restrict units to make sure currency is only ever added to other currency, the float roundoff errors, NaNs etc. will make you or your customers lose money.

But even without that: Great library! I’ll definitely make use of it once the need arises.

Arbitrary precision decimals would be tricky unless they were added to the language itself and were considered a number. elm-units works with any number type as the internal representation, which means you can use Ints if you want. My rough thought for working with currencies is that you could represent exact currency amounts using an Int number of cents etc. (or perhaps an Int number of “millionths of cents” or something if you need exact sub-cent arithmetic?).

I started playing around with some skeleton currency examples which would let you represent currency values as either an Int or Float number of cents as appropriate (and convert back and forth using Quantity.round etc.). That said, I only know enough about currency handling to know that some form of exact arithmetic should be supported - I’m certainly not an expert in the field. It could very well be that currency handling really does need an arbitrary-precision decimal type, in which case that may make more sense as a separate, independent type/package.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.