Measured - Type-safe, intuitive units of measure

I recently released Measured . A library that makes units a lot simpler and intuitive to work with. It uses the compiler to enforce correctness and lets you combine units into more complex ones using math operators. It is also extensible, making it easy to define your own units.

val velocity     = 5 * meters / seconds
val acceleration = 9 * meters / (seconds * seconds)
val time         = 1 * minutes

//  d = vt + ½at²
val distance     = velocity * time + 1.0/2 * acceleration * time * time

println(distance                ) // 16500 m
println(distance `as` kilometers) // 16.5 km
println(distance `as` miles     ) // 10.25262467191601 mi

println(5 * miles / hours `as` meters / seconds) // 2.2352 m/s

Give it a try. I’d appreciate any feedback.

7 Likes

Very cool! Could you explicitly declare the types for e.g. velocity, acceleration, and time in your example to be a little clearer on how it works?

This is what it looks like with explicit types.

val velocity: Measure<UnitsRatio<Length, Time>> = 5 * meters / seconds
val acceleration: Measure<UnitsRatio<Length, UnitsProduct<Time, Time>>> = 9 * meters / (seconds * seconds)
val time: Measure<Time> = 1 * minutes

//  d = vt + ½at²
val distance: Measure<Length> = velocity * time + 1.0/2 * acceleration * time * time

println(distance                ) // 16500 m
println(distance `as` kilometers) // 16.5 km
println(distance `as` miles     ) // 10.25262467191601 mi

The library also has typealiases for Velocity and Acceleration; so you could simplify it as follows:

val velocity: Measure<Velocity> = 5 * meters / seconds
val acceleration: Measure<Acceleration> = 9 * meters / (seconds * seconds)
val time: Measure<Time> = 1 * minutes

//  d = vt + ½at²
val distance: Measure<Length> = velocity * time + 1.0/2 * acceleration * time * time

println(distance                ) // 16500 m
println(distance `as` kilometers) // 16.5 km
println(distance `as` miles     ) // 10.25262467191601 mi

I tried to do something similar in Java for a project a while back for distances and velocity but it was … verbose to say the least. This is really nice, wish I had a project I needed it for!

1 Like

I’ve also found it valuable when working with simple units like time (see Doodle). It is much safer than relying on conventions: like all times are in milliseconds.

1 Like

Plus: you can enforce some constraints directly when the object is created. If you had for example a Length object you would likely exclude negative length. In my current project we are using little classes for almost everything and it really pays off. It is not only clear what exactly this String, Int or whatever means, but the type is also enforcing and documenting the rules.

Example:

data class VehicleCode(val value: String) {
  init {
    require(value.length == 20) { "Value must have 20 characters." }
    require(value[7] == '-') { "Value must have '-' at position 8." }
  }
}

Since creating such classes is so easy in Kotlin, I think this should become a common best-practice in Kotlin. And libraries like “Measured” make it even easier!

1 Like

You should read into Kotlin Arrow Meta’s refinement types. It adds a specific syntax for these with nice behavior (as in, the compiler can automatically cast from an Int to a VehicleCode if the requirements are satisfied, etc)

I would recommend to be consistent with the Kotlin standard library for type conversion!

The stdlib uses extension functions like Duration.toLongMilliseconds(): Long or Int.toLong(). It is more tedious for the library developer to cover all cases, but is far more intuitive to use as library consumer since the IDE can easily hint what you need!

Btw your lib is awesome :slight_smile: keep it up!

2 Likes

I highly discourage the us of as.
Either use a convert function or use the “in” operator.

distance = 3 kilometers
distance in meters

velocity = 3 meters / second
velocity in (kilometers / hour)
1 Like

I’d love to find a better pair of conversion method names. How would you use in? It maps to fun contains(...): Boolean. So I don’t see how it can work for conversion.

You are right :smiley:
in with backticks would still be better than as I would say.
What’s wrong about an infix function named convertTo?

1 Like

There are two methods: in and as. Both use back ticks. The in method coverts a Measure to a number instead of another Measure:

distance `as` kilometers // 16.5 km
distance `in` kilometers // 16.5

Most code will use in as the terminal point for a Measure. Or when passing one to code that uses raw numbers. The use of as should be less common, and likely pertain to displaying values like in the doc examples.

There’s already a perfectly good standard for conversion functions: .toXxx(). Everything else uses it; it’s universally understood, very clear, and reads well.

So why does this project do something different, something that (judging from this thread) causes some confusion, as well as awkward syntax (backticks)? Why is this any different from converting a Long to an Int, a List to a Set, or a DateTime to an Instant?

IMHO, infix functions and operator overloads are best used sparingly, only where there’s a clear reason not to do things the ordinary way, where the intent is crystal clear and matches any existing usage. This doesn’t seem to be one of those situations.

as seems a particularly bad choice, as it usually means something different: .asXxx() functions create a view of an existing object, one which reflects changes to that object (and whose changes are applied back to it). Your use looks like that, but behaves differently, which could lead to subtle bugs.

1 Like

Unfortunately the toXxx() convention doesn’t work well for this problem. Units are not a small, closed set that translate well to a set of functions. That means you’d have an explosion of very specialized functions to cover the wide range of units. Not to mention complex units like Newtons. Moreover, Units like Newton can be represented by a wide range of combinations. So the typealias for it would be something like:

typealias Newtons = UnitsRatio<UnitsProduct<Mass, Length>, UnitsProduct<Time, Time>>

And conversions would need to support any way of representing a Newton, say: kilograms * miles / (hours * minutes). This is a big reason why the framework does not provide specialized conversion methods.

Also, notice that the as and in methods take instances of Units and not the class itself. Saying value 'as' Newton does not make sense because you can’t convert between Unit types, only between unit instances. So Newton here is a type, but there are many ways to represent it that do not require new classes.

Bespoke conversions are not defined in the framework because of these differences. You can always add them for specific Measure<T>s as extension methods if you have a set that are particularly important for your use-case. But I’m not sure how much easier, or readable that is vs the current approach.

1 Like