Seriously, since everything I work in is
denoted as "cents" from backend to frontend,
I personally had never understood the need
This matches my experience.Obviously actual strict typing has its benefits, but as far as developer ergonomics are concerned, IME you can get about 95% of the benefits just by following a convention of including unit names in identifier names.
It's easy to see why this Ruby code might fail:
def launch_rocket(distance)
# what units are we expecting?
end
launch_rocket(42)
But this is virtually impossible to screw up, and reduces developer cognitive load: def launch_rocket(distance_km:)
# blahblahblah
end
# not happening unless developer consumes a large number
# of drugs
distance_miles = 42
launch_rocket(distance_km: distance_miles) # not happening unless developer consumes a large number
# of drugs
distance_miles = 42
launch_rocket(distance_km: distance_miles*1.6)
Aaaand we're off by 0.3924km. Enough to fit 4 football fields with a few meters left over. OopsiesUnits are hard. Never under-estimate the ability of programmers to think they're converting but get it slightly wrong.
I think even dumb gravity bombs in ww2 were more precise than “a few football fields”
If we're launching physical rockets then yeah, I would agree that that is probably not a job for dynamic typing.
appending units to identifiers helps, but it relies on a developer's eyeballs to spot any errors. It would be infinitely preferable if the type system would simply enforce this for you and developers not have to expend cycles reasoning about this stuff themselves.
Static typing is great, of course, I just don't agree that this can't be solved to almost the same level with languages with dynamic typing.
but it relies on a developer's eyeballs to spot any errors.
I'm assuming a relatively normal/sane environment where code is reviewed at pull request time, and there is a test suite. developers not have to expend cycles reasoning about this stuff themselves
It is not my experience that `launch_rocket(distance_km:)` requires any extra cycles whatsoever.I mean, as a coder, I'm going to have to be cognizant of the type anyway, even if we're doing `launch_rocket(distance:)` in a strongly typed language where `distance` is something of type `RocketLaunchDistance` or whatever.
I'm not arguing against static/strong typing in general or anything. Definitely lots of times when it is the clearly superior choice.
package main
type km int
func (distance km) launch_rocket() {}
func main() {
distance_miles := 42
// type int has no field or method launch_rocket
// distance_miles.launch_rocket()
// this works, but is obviously wrong:
km(distance_miles).launch_rocket()
}If you want to do a bit better, a simple way of handling it is to provide a separate type that handles the abstract quantity (distance, money, etc. - you'd probably want to be cleverer about money if you deal with multiple currencies though), and ensure that values of that type can be converted to and from numbers only when the units are explicitly specified.
So then you end up with functions that consume a distance looking something like this:
void launch_rocket(Distance distance) {
call_ancient_fortran_routine(get_miles_from_distance(distance));
}
And functions that create distances looking something like this: launch_rocket(create_distance_from_km(100));
What you now can't do is create a value that's of one unit, and pass it to something that expects another unit - the issue doesn't really arise, as Distance values themselves don't have specific units. They are a black box that somehow encodes a distance, and you specify the units used explicitly when initializing and you specify the units desired explicitly if retriving an actual number.(Turning a distance into a number would ideally be a rare case, though sometimes you'd need it. You'd provide maths functions for all operations required, so you'd hopefully rarely need the number for calculation purposes. For displaying in any UI, there'd be a function to convert it to a string that respects the user's locale and distance unit preferences. And so on.)
However it's definitely my observation that there are lots of times when we're passing numbers around and we don't need something quite as robust. I'm not sure that replacing every number in an application with a custom type is typically the best use of time, and certainly there are performance reasons why we might sometimes want to pass fundamental numbers around instead of complex types.
In those cases, adding units/types to identifiers offers an awful lot of value for close to zero effort.
The case you claim requires drugs will happen
eventually even if everybody is sober. I've seen
it many times, and I doubt I'm the only one.
I've never seen it, but that's probably because in my 25 years of writing code I have rarely seen the convention followed in the first place because coders tend to be aggressively disinterested in writing maintainable code.But seriously, if `launch_rocket(distance_km: distance_miles)` eludes the original coder and the code reviewers and future coders working with that code and it eludes your test suite... damn. You've got big problems.
Seeing functions with 5-6 positional arguments makes my skin crawl even if they have strong types.
function deduct(int cents) { ... }
int dollars = ...
deduct(dollars);
You need something like (Apps) Hungarian notation [1] as a minimum - or even better, subtypes of primitives like in Go to represent units in a typesafe way.An expressive type system would allow you to define both a cent and dollar types, s.t. assignments of those types to each other without conversion would fail.
In a way, it is a way to have the computer validate apps Hungarian rather than trusting the programmer (well, it’s more, but for this argument).
Go’s type system is anachronistic, compared to what modern language provides (but then, all of go is anachronistic on purpose. The usefulness of this purpose not to be discussed here).
Sorry for the misinfo!
This is a fantastic bit to tack on for divisive topics, I'm stealing it.
You can do similar things with other dimensions like “length”.
Or you can go whole hog, and have a single “type” for unit-aware values from which you can only successfully extract a unitless number by specifying a unit which is dimensionally compatible.
But most projects won’t do any of these because they will start out thinking they don’t need it, and by the time they realize the value they’ll think the cost of converting existing code is too high.
Any particular currency can be modelled simply as a single dimension; “money” more generally is more complex. You can either use a single currency of account, track exchange rates for other currencies with it over time, and convert other currencies into it based on the time applicable to the event, or you can track each currency as a separate domain and convert based on the applicable exchange rate for a particular purpose ad hoc based on the specific situation. (There’s probably other approaches that work, but those seem to be, in outline, the most obvious.)
The money type should be compatible with generic interfaces so existing sorting and aggregation functions, for example, can directly on them.
class Cents(int): pass
def send_money(amount: Cents):
print(f"Sending ${amount / 100}")
send_money(42)
That’ll immediately fail validation without you needing to make sure you have tests which would catch every possible problem like that.expected "Cents" [arg-type] Found 1 error in 1 file (checked 1 source file)
In a language which has strict typing, you wouldn’t even be able to compile it.
1) make you do the check
2) only require you to do the check once
COBOL was pretty good for dealing with money.
(a quick google did not help, managed to lead to examples of COBOL manipulating the first five letters of the alphabet ...)
To give an illustrative example, what's 2/3 of a dollar? 66 cents or 67 cents, one or the other, choose the same one you would choose with pencil and paper. Now add 33 cents, did you "overflow" the cents and need to increment the dollars?
Yeah, you can achieve the same thing with binary by constantly checking ranges of numbers, but the difference is, BCD when you screw up your code produces errors similar to adding numbers by hand, errors recognizable by your non computer literate accountant; binary screwups will produce a different unrecognizable pattern of errors.
the way it worked was pretty straightforward, just like 4 bits is hex 0-F and 8 bits is 0x00 to 0xFF, a BCD byte is 00-99 and you just never have the patterns for A-F. This was enforced in hardware, in the CPU/ALU
in terms of multi-currency, same thing, you'll see the same familiar rounding problems as traditional pencil and paper currency changing systems.
Also the same set of issues extends to fixed point implementations of "floating point"/"decimal fraction"/"rational number" systems more common in engineering. 1/3 is a .33333.... repeating fraction; 1/5 is .2, no repeat, because 2x5=10 base 10. In binary, 1/5 is a repeating decimal, not good for comparing results, rounding, etc. And you can easily see that the same issue does apply to currency too (it was my example above with 67 cents), it's just a bit less visible because it's less common to use extended fractional amounts.
In particular it perfectly represents numbers which are commonly used in modern commerce, like 19.99 or 1.648 (the current price per litre of fuel near me). It's not great at other numbers like pi or 1/240.
On consideration, I think my COBOL compiler's ability to define arbitrary-precision fixed-length numerical variables wasn't down to the use of BCD; you can do that with other binary encodings. But I worked for Burroughs at the time; their processors had hardware support for BCD arithmetic, so it was fast. The debugging convenience came with no great cost.