I may be misunderstanding this.
I may be misunderstanding this.
The advantages are:
- An integer doesn't tell you if you are talking about seconds, milliseconds or anything like that. What does sleep(500) means? Sleep 500s or 500ms? sleep(500_ms) is explicit
- It provides an abstraction. The internal representation may be a 64-bit number of nanoseconds, or a 32-bit number of milliseconds, the code will be the same.
- Conversions can be done for you, no more "*24*60*60" littered around your code if you want to convert from seconds to days, do fun(1_day) instead.
- Safety, prevents adding seconds to meters for instance. Ideally, it should also handle dividing a distance by a duration and give you a speed and things like that.
Under the hood, it is all integers of course (or floats), which is all machine code, but handling units is one of the things a high level language can do to make life easier on the human writing the code.
Eg
func transferRegularly(dollars(10000), days(30))
Meaning clear. func transferRegularly(10000, 30)
Meaning obscure, error prone and potentially costlyEg. you could simply do
transferRegularly(amount=10000, period_in_days=30)
I am always amazed how new languages never pick up this most amazing feature of Python.Though obviously, this code smells anyway because 1. repetitive transfers are usually in calendar units (eg. monthly, weekly, yearly — not all of which can be represented with an exact number of days), so in Python you'd probably pass in a timedelta and thus a distinct type anyway, and 2. amounts are usually done in decimal notation to keep adequate precision and currency rounding rules (or simply `amount_in_cents`).
Still, I am in favour of higher order types (eg. "timedelta" from Python), and use of them should be equally obligatory for unit-based stuff (eg. volume, so following the traditional rule of doing conversions only at the edges — when reading input, and ultimately printing it out).
One problem with this approach is refactoring. If you wanted to refactor your example with the parameter "amount_in_dollars" then you would either have to continue maintaining the legacy "amount" argument, or break existing code.
Guess how I discovered this issue :)
In practice, with good naming it won't make much difference and only shows up when comparing the docs (or intellisense) for an API with how it is actually used.
dollars(10000) is still better than this example, because: 10000 what? Pennies? USD? EUR?
Now you can say how there are also AUD, CAD...
The point was simply that if units are needed due to lack of specific type being used, it's nicer to have that in the API when language allows it.
As per the article, if you dont have proper names and just an 'int', that int can represent any scale of time...seconds, days, whatever.
In python youd need something like mypy, but in rust you could have the compiler ensure you are passing the right types.
You could perhaps newtype using a class (so you can instanceof) or tag via { unit: 'seconds', value: 30 } but that feels awful and seems to be against the ecosystem established practices.
This is indeed one of my gripes with TS typing. I'm spoiled by other languages, but I understand the design choice.
export interface OpaqueTag<UID> {
readonly __TAG__: UID;
}
export type WeakOpaque<T, UID> = T & OpaqueTag<UID>;
And then use it like this to create a newtype: export type PublicKey = WeakOpaque<Uint8Array, {readonly PublicKey: unique symbol}>;
To create this newtype, you need to use unsafe casting (`as PublicKey`), but you can use a PublicKey directly in APIs where a Uint8Array is needed (thus "weak opaque").Originally, the type signature was
type Props = {height: number}
This naturally raises the question - number of what? Does "50" mean `50px`, `50vw`, `50em`, `50rem`, `50%`?I've ended up changing the component to only accept pixels and changed the argument to be a string like this:
type Props = {height: `${number}px`}
Of course, if passing a "0" makes sense, you could also allow that. If you want to also accept, say, `50em`, you could use a union-type for that.I think this could actually work for other units as well. Instead of having `delay(200)`, you could instead have `delay("200ms")`, and have the "ms" validated by type system.
Maybe the future will see this getting more popular:
type WeightUnit = 'g' | 'grams' | 'kg' | ...;
type WeightString = `${number}${WeightUnit}`;
function registerPackage(weight: WeightString): void;Because it's React, the expectation is "px", using a string with a suffix to override it: https://reactjs.org/docs/dom-elements.html#style
Instead of a type for seconds, you'd want a type for all kinds of duration regardless of the time unit, and with easy conversion to integers of a specific time unit. So your foo() function would be taking a Duration as an input rather than seconds, and work correctly no matter what unit you created that duration from:
foo(Duration.seconds(3600))
foo(Duration.hours(1))
foo(Duration.hours(1) + Duration.seconds(5))You can't add seconds to kilometers, or to temperature, or to how blue something is.
The world is far, far from ideal.
The units are also extremely clear when they are carried by the type instead of the variable name, where a developer could e.g. change some functionality and end up with a variable called `timeout_in_ms` while the function that eats this variable might expect seconds for some reason.
If it is typed out you can just check if the function performs the right action when passed a value of each time unit type and then you only ever have to worry about mistakingly declaring the wrong type somewhere.
But wether you should really do all that typing depends on how central units are for what you are doing. If you have one delay somewhere, who cares if it is typed or not. If you are building a CNC system where a unit conversion error could result in death and destruction, maybe it would be worth thinking about.