The only people I've ever met that think this is unnecessary are also the same people that ship a lot of bugs for other people to fix.
The only people I've ever met that think this is unnecessary are also the same people that ship a lot of bugs for other people to fix.
I find feelings about this depend a lot on how much ceremony the type system involves; and just how many units and references to them there are in the system.
Asking bash coders to write "sleep 5s" instead of "sleep 5" - I doubt you'd get any objections at all.
But if you're putting a foo.bar.Duration on a getDefaultTimeoutFromEnvironment on a setConnectTimeout on a HttpRequest on a HttpRequestInitializer on a NetHttpTransport on a ProviderCredential to make a simple get request? People who've come from less ceremony-heavy languages might feel less productive, despite producing 10x the lines of code.
request = NetHttpTransport.Request()
request.setConnectTimeout(getDefaultTimeoutFromEnvironment().seconds)
which is verbose, but at least it's fairly clear.And yes I'm calling a class named HttpRequestInitializer silly, I don't care if some language decided it should exist.
My last shop was a Go shop and one test I think shows this off was an SMTP server and we needed to test the timeouts at different states. The test spun up half a dozen instances of the server, got each into the right smtp state and verified timeouts in under 10ms.
The environment that set the timeout would either be "timeout_ms=500" or "timeout=500ms" (or whatever). This is where you handle that :)
Of course if the setConnectTimeout method was part of the same application you could just pass the timeout directly, but at the boundary of your application you're still going to have to specify at some point which unit you want.
(although getDefaultTimeoutFromEnvironment was artistic license on my part)
Edit: Also, overriding a class method dynamically inside a function? I usually program python these days and even I think that's wild.
request.setConnectTimeout(getDefaultTimeoutFromEnvironment().seconds)
This is actually a very good example of what not to do, with the mistake being in whoever implemented setConnectTimeout()I actually don't know this particular API, but I'm used to timeouts being in milliseconds, so that code looks wrong to me.
Much better API, and what the article is talking about, is to change this to:
.setConnectTimeoutMilliseconds(getDefaultTimeoutFromEnvironment().seconds)
Now the mistake is obvious, and even if the original developer doesn't notice it will stick out in a PR or even a causal glance. .setPricePerMassInCentsPerKilogramsWithTimeoutInMilliSeconds(100,2,300) .setPrice(priceInCents=100, massInKg=2, timeoutInMs=300) .setPriceInCents(100);
.setMassInKilograms(2);
.setTimeoutInMilliseconds(300);
or .calculate({
priceInCents = 100,
massInKilograms: 2,
timeoutInMilliseconds: 300,
}); calculate(100cent, 2Kg, 300s)
calculate(0.1dollar,4.7lb/*approximate*/,5min)
calculate(something.price(),whatever.weight(),options.getDuration("exampleTimeout"))
calculate(USD(0.1),Kg(2),Minute(5)) request.setConnectTimeout(getDefaultTimeoutFromEnvironment())
with getDefaultTimeoutFromEnvironment() returning a Duration.Ideally this is consistent throughout the codebase, so that anything that uses a primitive type for time is explicitly labelled, and anything using a Duration can just be called "Timeout" or whatever.
Ideally it'd be as simple as:
typealias Dollars = BigDecimal
typealias Cents = Long
that's valid Kotlin, but the equivalent is doable in most languages nowadays (Java being a big exception).
I don't know Kotlin, but in most languages if you alias 2 types to the same base type, for example Seconds and Minutes to Long, the compiler will happily allow you to mix all 3 of them, defeating the protection full types would bring.
> typealias Cents = Long
If I ever have to deal with monetary values in a program where someone thought this was a good idea, ... well, it really won't be the worst thing I’ve ever dealt with, but, still.
(If you have dollars and cents as types in the same program, they darn well better work properly together, which is unlikely to work if they are both just aliases for basic numeric types.)
typealias Cents = Long
typealias Meters = Long
Cents(3) + Meters(4) = 7L
that's exactly the thing we want to prevent.the classes shouldn't be for units at all, the type should represent the type of unit.
so instead of a class Seconds you should have a class Duration, instead of class Meters you should have a type Distance. that's because the unit of the different types can be converted between each other.
Dollars and Cents are a bit of a bad example because currencies can't be easily converted between each other, as conversion is dependent on many factors that change over time. meters, yards, feet, lightyears, miles, chain, furlongs, whatever describe a multiple of the same thing though, so a different type for each unit isn't necessary, as the input that was used to create the instance isn' usually needed. the counter example would be datetime with timezones - a datetime with a timezone convers a lot more information than the datetime converted to UTC.
We keep laughing about "if we engineered bridges as we engineer software" ... the truth is that the areas where correct software matters tend to write very robust code, and the rest of the industry would be well advised to take notice and copy what they see.
Of course, writing robust code is a skill, and it takes extra time.
I don't agree.
There is a good reason that aerospace industry writes robust code - in invests time (money) to avoid disasters that could cause, among other things, loss of human life.
On the other hand if for example some webform validation fails because the code was written as fast (as cheap) as possible, who cares really.
That is just a tradeoff, in aerospace industry you spend more to lower the risk, somewhere else you don't.
Who knows. Maybe a billion dollar company that can't fulfill orders. Maybe a million people who suddenly can't use their bank online.
You can probabbly find a some specific non-critical case in aerospace industry, but surely based on that example one would not suggest that the whole aerospace industry should just copy what they see in frontend dev.
Context matters, there are exceptions, but the standard practices are based on average scenario, not on extremes.
>> I don't agree.
>> [web form validation example]
>> That is just a tradeoff, in aerospace industry you spend more to lower the risk, somewhere else you don't.
> I'm just saying that "oh, it's just web stuff so it can't be important" is ridiculous.
This feels like a straw man.
The original argument is that the rest of the industry (that includes web, but a lot of other parts also) should copy what they see in aerospace industry.
I believe that would not be appropriate for the rest of the industry to just copy practices from any other part because each segment has its own risk factors and expected development costs and with that in mind developed their own standard practices. Nowhere did I state that the "web stuff can't be important" nor that there is no example of web development (form validation) where the errors are insignificant.
That said, I will go back to the "billion dollar company that can't fulfill orders" / "million people who suddenly can't use their bank online" catastrophe; this happens all the time. Billion dollar company doesn't fulfill orders, error is fixed, orders are fulfilled again, 0.0..01% of revenue is (maybe) lost.
In aerospace industry a bug is deployed, people are dead. No bugfixing will matter after that moment.
How can this two industries have the same standards of development?
I don't use libraries to enforce it though, we did try this but found it quite clunky: https://pint.readthedocs.io/en/stable/
One piece of equipment I just finished working on measured and displayed vacuum at different sensors in PSIg, kPa, Pa, mmHg, and inHg. The same machine, the same measurement at different stages in the process, five different units!
Even more fun is that sometimes units alone aren't sufficient. We need to know which coordinate system the units are being used in: screen, world, document, object-local, etc. It's amazing how many different coordinate systems you can come up with...
Or which time stream a timestamp comes from, input times, draw times (both of which are usually uptime values from CLOCK_MONOTONIC) or wall times.
I may be misunderstanding this.
You can't add seconds to kilometers, or to temperature, or to how blue something is.
The world is far, far from ideal.
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
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.
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.
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))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.