Hacker News new | past | comments | ask | show | jobs | submit login

Using types that encode the units, as suggested in OP, is strictly better.



You're both right, but not all languages give you the choice. And a lot of types are implemented badly. For example as a subclass of a number class, so that `amountDollars + timeSeconds` and other nonsensical statements aren't errors.


I'm not optimistic on unit types myself. I've found that unit-named variables often with transparent type aliases as documentation (type Sample = usize, type Amplitude = i16/f32) have many of the advantages of distinct units, without their downsides compared to bare numbers. In several projects where I've used naming and transparent aliases comprehensively, I don't recall ever letting a unit mistake escape from my local tree into master, since I reread my own code when committing and merging. In one case (https://gitlab.com/exotracker/exotracker-cpp/-/blob/dev/src/... used to have two EXPLICIT_TYPEDEF) I did add distinct units because I found I was mixing together two types too often during development. Though I find that unit types come with significant disadvantages (ergonomic and semantic flaws), making them far from strictly better than bare numbers (much like Rust is far from strictly better than C/C++/Zig):

- You need an implicit conversion to eg. size_t, otherwise you can't pass (smp: Sample) into array indexing like (amplitudes[smp]) or data slicing, without an extra conversion or accessing the underlying value like (smp.v). But you can't allow (smp += midi_pitch) to convert both arguments to int, then cast the result to Sample when assigning.

- You need some conversion to allow (smp + 1) with type either integer (convertible to Sample) or Sample, unless you want to annotate all arithmetic with boilerplate like (smp + (Sample)1), or (smp.v + 1). I've experienced this problem in my own code, and had to write (smp.v) when my compiler saw (smp + 1) and told me it didn't know whether to wrap 1 or unwrap smp.

- Expressions of type Amplitude * 2 should have type Amplitude. Go's time library gets this wrong, where multiplying Duration * Duration = Duration, which makes sense if Duration is an integer like i32 or i64, but not if Duration is a unit system dimension.

- (not a regression but a limitation) Units won't stop you from adding two temperatures in Celsius. To fix this you need separate coordinate and displacement types, which is a new pile of complexity.

- You may want distinct types for "samples/sec" and "cycles/sec". Modeling this in type systems has multiple current approaches, all of which rely on language support (F#) or complex type machinery I've had issues with.

- You can't easily convert between slices of f32 (like an audio buffer provided by the OS), and slices of Amplitude<f32>. Or worse yet vectors of f32 and Amplitude<f32>. (This problem affects bulk data in collections, more than scalar types generally passed and returned in the stack.)


If you're the type of moron who will write:

  chargeCustomer(amountDollars: valueInCents)
I have little faith that types would help you. You could simply do:

  chargeCustomer(amount: Dollars.from_int(valueInCents))
And create the same bug.

Positional arguments is just as big an evil as non-typed units, IMO.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: