Had exactly that bug in production, was using ruby on rails & active merchant, and some version change in ActiveMerchant switched from cents to dollars for one of the integrations.
Our test harness didn't catch it (weird combination of reasons, too long ago for me to remember the details) & it rolled out.
Shortly thereafter I get an anxious customer call that we'd charged their debit card $2500.00 instead of $25.00 and they'd gotten an overdraft notice. At first I was incredulous ("how is that even possible!?"), then I remembered that we'd just version bumped ActiveMerchant.
My endocrine response as I realized what must have happened was amazing to experience - the sinking feeling in my gut, hairs standing up, sweaty palms, dread, pupils dilating, and my internal video camera pulling back poltergeist-style in a brief out-of-body experience.
Unfortunately I've run into a lot of folks that take major issue with the later because of 'magic numbers' or some similar argument. In tests I want the values being checked to be be as specific as possible.
One of the tenets of unit testing, I don’t know if I derived this for myself or got it from someone, is that you get a lookup on the right side of the expectation or the left, but not both.
There are too many yahoos out there writing impure functions or breaking pure ones that will mangle your fixture data. And the Sahara-DRY chuckleheads who see typing in 2500 twice (but somehow are okay with typing result.foo.bar.baz.omg.wtf.bbq twice) as some crime against humanity exacerbate things. Properly DAMP tests are stupid-simple to fix when the requirements change.
That's my approach as well. My idea is I can change the numbers, and it still better come out right. It forces you to think through (and code) things more clearly. It's too easy to write a test with hard-coded numbers that "just happens" to come out right.
That's a good enough (initial) workaround if you're in an environment where you're in the minority (or alone) w/ your opinion and you still want to do the right thing personally.
I would advise you to try and convince your peers though and teach them the better way because I suspect that the people that you're fending off would not do what you did but rather just go w/ the one
expect(account1.balance).to eq account2.balance
Now while parts of the code base do the right thing (in a slightly long winded way), the rest of the code base written by these other people is still using bad tests.
Various things like "manual to change them" that make "magic numbers" bad in regular code make them good for testing (or at least less bad, a constant for that is still what I'd usually use, but a pretty specific constant, sometimes at the unit test level - shared ones get dicey).
Agreed on the ease of having problems of using variables on both sides.
One of the biggest ways that test code is not production code is that test code is only read by humans when the tests are failing. Whereas any time I'm working on a regular feature I am likely to be looking at log(n) lines of our codebase due to the logic that exists around the code I'm trying to write, and changing loglogn lines of existing code to make it work - if the architecture is good.
Code that is write once read k < 10 times has very different lifecycle expectations than code that is constantly being work hardened.
What the complainers often like to do is hoist variable declarations out of the local scope. So to them having a test suite that uses 1500 in four places is wrong, and up to that point they are perfectly right.
The trick with the constants is that if they are declared and used in the same test scope, then the data is a black box. Nobody else 'sees' it, nobody interacts with it. The only time that's not true is when there's false sharing between unit tests and those tests are fundamentally broken. In that case the magic number is not the problem, it's the forcing function that makes you fix your broken shit.
I made an account just to tell you that I was able to almost feel & experience what you 're describing just by reading your comment. There should be a market for this hah
Reasonableness checks are nearly absent these days. Warning the user "that's a lot of money/a date far in the future/a large quantity.[1] Are you sure?" detracts, I suppose from the UX. Except it really doesn't.
1. "Reasonable" varies according to the particular situation (customer's order history, credit rating, the normal range of quantites for the product, etc.)
I think it's more that it's a game of whack-a-mole. There are an infinite number of possible scenarios you could warn about, and each one carries a small cost to implement, a small cost to maintain, and a risk of false-positives. Which ones are worth implementing can be hard to know ahead of time (implementing ones that have actually caused problems would be one strategy for narrowing it down, but the point remains that it's not as simple as "just check for all the unreasonable states")
Frankly, the ActiveMerchant team broke your trust. There's two things you can do with people you don't trust. Either cut them out of your life, or start verifying everything they tell you. I've dumped libraries with this sort of chaotic 'refactoring' (or not picked them because I could see they had made such poor initial choices that it was inevitable). I've also written unit, integration, or smoke tests for third party code with a high Precautionary Principle quotient, sometimes with its own separate build pipeline that takes green versions of our code to run them against latest of theirs. And for any code we got from a customer or business partner? This is practically a requirement for my sanity. They think that because money is changing hands they can do whatever they want and we just have to eat it. We don't have to eat anything.
That doesn't catch the problem the moment it happens, but most times that's sufficient to catch it before anything hits production.
I also had this problem—with the same stack—but the issue was quite subtle. It was due to an upgrade of the Money gem that changed the way "cents" were handled.
In our case we had test coverage, including integration tests with VCR recordings to the payment gateway. But the problem was that the bug only affect Japanese Yen, and we did not cover every single currency.
I'm gonna be honest, I had never understood the "strict type" argument until right now. Seriously, since everything I work in is denoted as "cents" from backend to frontend, I personally had never understood the need so I'm part of today's lucky 10,000th.
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)
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. Somebody is tired, or they get the computer to perform some bulk change and get it wrong, or they are doing some rote transformation and they miss a case.
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:
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.)
What you're suggesting is obviously more foolproof for sure.
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.
I wish more languages had keyword arguments. In the unfortunate languages I've used without them (C++/Rust), I usually avoid mixing up units/types through naming, but in the rare situations I do find myself confusing similar but distinct dimensions despite clear naming, I selectively opt into custom types where their utility justifies their interoperability/arithmetic/conversion downsides.
> [follow] a convention of including unit names in identifier names.
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.
If you stick to just a few units throughout the system (ideally, use a consistent base unit everywhere and convert any external measurements to it), you can avoid the rest of the problems.
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.
The keyword is “almost”, and whether you need stricter typing depends how much you care about software correctness (or to be pedantic, the class of problems that the typing system can fix for you in the language in question).
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.
It's within a significant digit of the specified distance, though. A few football fields of error seems pretty good to me for 42 miles. What's the required accuracy and precision for this rocket launching system?
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()
}
Nope, this will lead to the same problem that the OP has discovered, viz., that a dependency update creates silent errors. Unless the argument is a keyword, but everyone is too lazy for that.
If you encode your test in a type then you don't have to write it separately. Even in python you can use types for this sort of thing it will blow up at runtime but so will your test. Just have a US Money type that won't return a number unless you call as_cents() or as_dollars(). Now much harder to misuse.
I'm also in the camp that believes that functions or methods that take more than 2-3 (positional) arguments should really take a structure with named keys to avoid this. Humans are bad at lists.
Seeing functions with 5-6 positional arguments makes my skin crawl even if they have strong types.
That won't always work unfortunately, particularly if you have to work globally. For example the fiscalization requirements for cash reporting in Germany specify that all of your transactiona have to be done to 6 decimal places (for things like discounts.)
This bug could easily happen in a typed language though.
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.
I think you’re confusing expressiveness of types with static/dynamic typing (the latter are also typed!)
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).
Yeah, defining "dollars" and "cents" (and over units) types are actually a really good solution for this, it means that you can never pass a value of one unit when the function expects another.
You can have a single type for “money” (or maybe just “us_money”) with separate cents/dollars/mills factories and accessors, and no access to a numeric value except through the accessors.
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.
Yes, good point. This in fact what I've done previously in an application that was dealing with monetary values. They were basically fixnums with a unit piggybacked on, plus some money-specific operations.
The money type should be compatible with generic interfaces so existing sorting and aggregation functions, for example, can directly on them.
In physics, dimensionality makes sense. How would one handle money? Can it be represented the same way? A new dimension, next to length, time etc? It would allow to express money per time, eg dollars per second, for example.
> How would one handle money? Can it be represented the same way?
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.)
You don't need "strict typing" to handle money; in the old (COBOL) days, we used BCD to represent monetary amounts with arbitrary precision. When they took away BCD, we were stuck, if we wanted to build a system that could represent a large sum correctly in both dollars and yen.
You're right; but COBOL BCD types allowed arbitrary precision, and it was super-easy to debug data; the hex represention was the same as the decimal representation.
BCD is simply "work in base ten on a base two digital computer". What made it good is that it enforces a discipline with the same pattern of rounding errors as base ten arithmetic on pencil and paper. This was particularly attractive when computers were new and replacing "doing it by hand". Bankers were nervous about the new systems screwing everything up, and they wanted the new system to demonstrate it would produce the exact same results as the old system.
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.
I appreciated BCD because the in-memory data represented in hex (as by an 80's era debugger) was exactly the decimal value. As I recall, debuggers of that time only understood two datatypes: ASCII characters and hex octets.
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.
Binary Coded Decimal allows for perfect representation of numbers by not restricting you to 4 or 8 bytes. It trades speed and memory efficiency for precision and simplicity.
It allows, like all encodings, a perfect representation of some subset of real numbers but not the rest.
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.
You still don't know whether something is dollars or cents. Some things are conventionally priced in dollars, others in cents, and your customers are going to be very unhappy if they have to enter or read the price in the "wrong" unit.
I was working with [top 5 bank in the US] and hit the same issue with regards to their interest rates between products.
Some represented interest rates as a simple number "5%" others as a decimal "0.05" and others as basis points (500). It caused some HUGE problems internally as less clueful loan officers were plugging 0.05% interest rates into formulas for customers. The naming was just as bad. We had columns and fields called intRate, int_rate, interest_rate, and of course iRate.
I asked people if they were irate over the problem. No one laughed. :D
Just to say it, for measurements that take a unit I am hardcore that you should include the unit in the variable time:
`timeSeconds`
`distanceMeters`
`amountDollars`
Have unit tests and everything, but also write your code so that when someone reads it they know as precisely as possible what's going on without other documentation.
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.)
At that point, why not have dedicated types and get rid of primitive ones? It shifts all that work to the compiler, whose job it is to excel at this kind of stuff (adding cents to penny types does the right thing automatically etc).
Because you still have to co-exist with other software beyond the type system's boundaries. The amounts are going to be converted to/from JSON, put in and out of databases, show up in logs etc.
When it comes time to invoke Amount.fromCents(..) at the boundary of the system, it's nice if the front-end posts a variable called amountInCents.
Would you also add other type-related aspects into variable names? `firstNameString`, `ratioFloat`, `colorEnum`? These things should be types, not easy-to-ignore type-encodings into variable names..
I encountered a similar bug at the same company with far worse results.
Won't say any specifics about the product impact, but our backend passed around two different kinds of user IDs. Each user had two different IDs, and the ID spaces overlapped. User Alice could have an ID in space 1 that is the same as Bob's ID in space 2.
At some point, at least one function expected a "space 1" ID but was being passed a "space 2" ID. This meant that content meant for Alice was shown to Bob and vice versa. None of the data was private in this case, so there was no legal problem, but it was pretty embarrassing. I suggested using strong types for the ID spaces instead of `int`, but left the company before implementing any of that.
Regardless, this makes an excellent case for strongly typed wrappers as you mention at the end.
Our general approach is to use UUIDv4 for identifiers (so the chance of mistaking-one-for-another instantly leads to "not found"), but sometimes you don't have a choice. In those cases it's super-important to have strongly typed wrappers.
Similar kind of thing, one company I worked for we had two smart-card/badges, I can't remember why. It may have been nothing more unusual than one was the building card, one was the company one.
They had an overlapping ID space. Swipe the wrong one on the printer and you got someone else's print job. In my case I accidentally caused a Senior Director's print job to be printed, and luckily it wasn't anything sensitive.
I had no end of trouble trying to get IT to accept that this was actually a problem.
Yes, you can, but you shouldn’t to solve this problem, because typealiases aren’t new types, just–as the name suggests–aliases, so anything aliased to Int is interchangeable with Int and everything else aliased that way, so you still have exactly the same problem.
Also, cents * cents shouldn’t be cents (though cents * unitless ints should be).
That's not quite the same thing, as in f# a unit of measure actually makes not interchangable. While a type alias, afaik, literally is just an alias, without preventing you to do something like that:
typealias Cent = Int
typealias Euro = Int
SomeEuro = SomeCent
In f#, measures aren't bound to any specific numeric type either.
I think I like the duck typing in TypeScript more than I dislike it.
But I still wish I could say “this function accepts a type called RobotName. It’s a string, but so is RobotUuid, and we don’t want that. So only accept, strictly, objects typed as RobotName.”
Way back in the 1990s I worked in a place where we had to use Ada and follow a strict style guide. The guide prohibited using raw numeric types (such as "unsigned int" or "double" in C-like languages) and required creating a new type (https://en.wikibooks.org/wiki/Ada_Programming/Type_System#De...) for each different quantity, such as temperature or speed. Then you couldn't assign a speed value to a temperature variable, or compare them to each other or do arithmetic, unless you specifically define what the operators mean.
It felt like a lot of busywork, but it did prevent some classes of bugs.
In the mid-2010s I worked at a company that switched from using raw ints to refer to database rows to using (in C# terms) "Id<FooTable>".
Across the entire codebase, we discovered an entire class of bugs that only never cause any issues because all the important rows in all the important tables had Id 1 (e.g. Currency 1 was USD and Country 1 was USA) - so in a few places where the ints got mixed up, the correct row was still accidentally looked up in the wrong table).
Something I learned long ago, but occasionally disregard to my peril: if you see something that looks like a bug, but the code/system still works, stop and figure out why it works.
It's very easy to mentally shrug and move on, but more often than not it comes back to bite you; maybe it's a code path that's rarely triggered, e.g.
I have the same policy for similar circumstances. Sometimes, something isn't working, and I make a change that fixes it, but I didn't expect that change to fix it. Almost always it's worth me investigating why it's now working, because it indicates a deeper problem that would come back to bite me.
Many C++ code bases still do this pervasively, it is a common practice to improve robustness. I don't know what it is like in Ada but C++ metaprogramming makes this not too onerous.
Or if you don't want to make an extra wrapper around every object:
interface RobotName {
_phantomType: "RobotName";
}
function makeRobotName(name: string): RobotName {
return name as any as RobotName;
}
function getRobotName(robotName: RobotName): string {
return robotName as any as string;
}
Presumably V8 is smart enough to inline the wrapper functions.
The closest way to get nominal typing in TypeScript is with type branding. I haven’t actually used this small library, but it illustrates the idea: https://github.com/kourge/ts-brand
This is actually one of the nice features about OCaml that comes in handy when you have a function that takes two arguments of the same type but don't necessarily need to make a whole new type for each of them. They are called Labelled Arguments [0] and when you call the function, the label has to be the same. I've found that using it can clean up code because it ensures that variables share the name across the codebase as well as making sure arguments don't get mixed up.
Yeah just means you have to rely on linting and your ide to catch those errors. And hopefully the rest of your team does the same thing.
Tho I suppose if it is really important you can put an assert there but I'm not familiar with that wrt typescript, maybe the transpiler would kill that?
I've done the occasional type checking in that way in similar languages, it is kind of self documenting too. 90% of the time duck typing is what you want.
If robot0 name is AAA and uid Is BBB, and robot 1 name is BBB and uid AAA, and you call the function checkRobot('AAA'), what sort of assert exactly could differentiate between a name and a uid?
One of the first programming languages I learned was TI BASIC on the TI-89. It had an excellent units (refinement types) system: you could define an expression as a "unit" by prefixing a variable with an underscore, and it came with a bunch of built-in units. A unit expression could thereby automatically convert between types as needed, outputing the base unit.
E.g. `5_dollars + 8_cents` would output `508_cents`. There was also a conversion operator you could use to change the output unit, so `5_dollars + 8_cents→_dollars` would output `5.08_dollars`.
It even worked for more complicated derived units, so e.g. `8_m * 5_kg / (3_s * 4_s)` returns `3.333_N`.
I think TI probably copied that from the HP 48, which used more or less the same syntax. The only difference is that the calculation you show would not have been automatically reduced to _N, but you could use the CONVERT or UFACT commands to get Newtons from kg*m/s^2.
In practice, HP's UI made it a lot easier to manipulate quantities tagged with units: hitting the softkey for a unit would multiply the current value by that unit, and using the two shift keys you could either divide by that unit or convert to that unit. If you made a custom menu to put the handful of units relevant to your current problem domain all close at hand, you would need very few extra keystrokes compared with calculating without using the units system. That ease of use is vital; opt-in type safety should be as easy to use as possible, so that users aren't tempted to fall back to the simpler, less safe method.
Of course, templates are sufficiently complex metaprogramming to achieve this. But it's not a language built-in, or even part of the standard library.
F# has a refinement type system built in (which allows this), and of course Haskell and Idris are sufficiently capable in their type systems to trivially support it, but none of those are particularly mainstream general-purpose programming languages. Mainstream languages pretty much all need some sort of library for it, which is unfortunate since they're extremely useful in many common programming problems.
One would have to make a library with various classes representing various units, plus the (implicit) conversion between them. It´s probably something that exists somewhere.
I understand this whole scenario is simplified for story's sake but,
if there's old_func and new_func, and the new_func call in the else was added that morning, why would the same error of new_func getting the wrong amount of arguments happen weeks before?
I suspect the new function was an attempt to fix whatever was the backend problem (because before it would work the second time, but give the correct $250).
This blogger occasionally seems to throw in stories that are made up for the sake of making a point; this is likely one of them, given the inconsistencies.
I'm confused how this was able to give out money before the new code was submitted to production. The author claims that both she and her coworker tested it before the code was submitted and they ended up with the extra $25k. Was this code only executed on the front end? Were there no checks in the backend to prevent employees from just pulling out whatever money they wanted?
The way that things work at this particular company is that you typically test changes in this codebase on your dev machine, but usually the dev machine talks to a prod database.
The prod database is too large to practically have a second copy sitting around for testing. Also, if you tested on some pristine small test database you're going to end up missing bugs that would only manifest with actual prod data.
For some systems in that environment, the back-end for dev was actual production. Keep in mind that wasn’t handing out actual money, it was creating ads credit.
There are several things happening here worth breaking down.
The first is what I've seen called a "gettier"[1]. The idea of "justified true belief" which ends up being true, but not for the reason you thought it was true. That's the case of the first of the bug fixes: She'd exposed the problem with the first change, but it wasn't really the problem.
The second item of note is that one paper found 92% of catastrophic system failures come from buggy error-handling code.[2] Arguably this doesn't count as catastrophic, but $25K adds up.
The third and final item is the failure relating to the use of a primitive, number' instead of a domain-relevant type, like Money, Dollars, or Pennies. This concept came up as Value Object three days ago[3], which Ward Cunningham's CHECKS Pattern Language of Information Integrity, published in 1994, called Whole Value[4]. I've seen (and, as a young code, written) programs that are full of strings for everything, because that's how the they are represented to users and passed over (some kinds of) network services. This "stringly typed" code infests a project I'm currently engaged with, simply because the back end depends on a bunch of REST/JSON apis and never bothers to deserialize them, but passes them throughout large parts of the code completely unrelated to the api calls.
I put myself in this very position with some cache expiration times.
Almost everything in the server-side that is related to times for a particular client uses milliseconds. Of course, redis’s `SETEX` does not. It uses seconds.
The data got quite stale. But, much like this post, other bugs were uncovered and usefully fixed.
For some time the redis-py library had the increment and member argument order of ZINCRBY switched from what standard Redis uses. Of course we didn't notice this small difference.
Debugging this was harddd.
They later switched this around, luckily I read the changelog.
Worked somewhere with an admin tool that did something similar. The code had denominated money in cents, except for the Japanese Yen, which has no cents so the number used was just the number of yen. After someone refactored some related code, for a short period of time Japanese users got refunds 100x the amount they were supposed to be.
>I say bare numbers can be poison in a sufficiently complicated system
Indeed. It's interesting that the de facto solution to this is to key value pairs, where sometimes the key can be an array (e.g. json, xml, etc), and then one can (kinda) infer units from the key. But even this is insufficient, because inevitably the value is pulled out and it's context is lost.
This is, I think, a(nother) powerful argument for immutable values, and accreting structure losslessly with pointers. Like in Clojure. In general we our runtime should support values that have monotonically increasing amounts of metadata added to them during runtime, for example units, or more paths, such that any user of the value can interrogate that structure and find out what it meant to the last people to read or write the value.
This one brought up yet another tech memory to share from the past. I was the founding architect of a real-time payments company in the mid 1990s which at the time was processing for a very well known, even to this day, ‘use your email to pay’ company. My position as the architect and critical core software developer brought me into many problems that were others, both in business and in technology. As the leading technology based payment processor at that time along with our large volume we had a relationship with a bank that had another smaller processor which suffered a breach, one of the first widely reported. This smaller processor was "consumed" by us, unknown why nor can I recall atm but back then it was much different working with the large card brands and banks, and we therefore were transferred all their assets including source code. I was tasked with finding the areas of compromise within the software systems designs having already rearchitected our core systems to XML and microservices long before such ideas were even considered. In performing a source code review of what was a monolith written entirely in Visual Basic 6 I discovered whoever had written it decided that they would treat themselves to much greater than 25K of real fiat currency. Someone had added to the if statement on checking the credit card number that upon matching a specific series of numerics no outbound card brand API call was made and this transaction was auto approved, no other checks, and this code was in production. Those reading this far can likely deduce the potential of such a single if statement.
> This is yet another reason why I say bare numbers can be poison in a sufficiently complicated system.
In Racket, I tended to use keyword arguments, and include the units in the keyword argument. For example, one library used `:velocity-mm/s`. (The `/` character is an identifier constituent, not some special operator syntax.)
In Rust, I'm going to try out using language features for static checking of some units (with 0 runtime cost).
Yes, the newtype pattern in Rust works very well, say to wrap a string. The somewhat annoying thing is that it is born without any properties so you have to teach it to be comparable, etc from scratch.
The newtype pattern in Go is easier to use, but isn't strict enough - your wrapped string can still appear directly in a string concatenation without a cast. Any wrapped integer can still be used to index a slice! Considering how careful the language is with mixed arithmetic (can't add uint8 to uint16 without casting) this feels like an oversight.
This discussion reminds me that F# has units of measure support in the language. For example constants can be annotated with units[1]:
1.0<cm>
55.0<miles/hour>
Tangentially, it also reminds me that some popular languages don't have a binary coded decimal type for example go and java(?). C# and standard sql support decimals, so your $1.02 doesn't get approximated as $1.01999999
Go has at least 25 community decimal packages[2], but who knows which is a good one to use.
What amazes me is the software running the banking accounts. Whenever I go to an ATM machine, and I get the receipt, I never for a moment think they can be wrong by even one cent. Because they've never been wrong. From time to time you hear of a case where someone found a million dollars in their account, but this gets to be reported in newspaper. And to be honest I don't even remember how many years ago I read such a story.
I imagine there are tens of millions of people accessing their bank accounts every single day. A bank like Chase or Bank or America, or Wells Fargo probably processes millions of transactions per second. With essentially zero errors. Meaning 99.99999999% correctness, with too many nines to count.
How is that possible?
Not to mention, these guy are probably under hundreds of hacker attacks per day.
Makes a good case why most programing languages should not provide simple numeric types - being the types we're so used to using that we don't even think about it. Squashing everything into one or two numeric types leaves us wide open to misinterpretation and confusion.
Instead, provide types for length, time, currency, age, angle, count, etc. And provide suitable operations on those types. Then inadvertently passing a pennies as dollars, or adding a time to a length, would be flagged as an error early.
(To my mind, the only languages that should provide a "number" type are systems specialised for mathematical use, where it's the mathematicians' fault if they shoot themselves in the foot.)
F# has units of measure types which seem fairly pleasant: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref... along your ideas. You can also do your own version easily in something like Haskell. At first thought it'd be much more challenging in everything else assuming you don't want to encapsulate everything in voluminous custom classes or something.
Transformer boxes, Nuclear waste, Highly acidic compounds, High energy lasers, choking hazards.
The oldest debate: should there be a money primitive in the type system?
I mean, availability breeds use, use breeds awareness, awareness breeds or enforces proper use. A currency/money type would be a pretty clear label to ward off a whole suite of stupid bugs like the one described in this post.
I think there are some other things like fixed-point rather than floating-point decimals for storing/manipulating currency. IIRC, the number 0.1 cannot be precisely represented with floating point system, but that just may be an old wives tale at this point.
My primary codebase ingests data with a mix of seconds, milliseconds, and microseconds. We've held it at bay for a while, but it requires some mental overhead unless we've baked the unit into each variable name.
We're likely to normalize on milliseconds, but at the same time, the implication of floating point arithmetic on all our μs numbers isn't ideal.
In a similar vein, in the early days of launching Relay[1] I ended up spending a couple weeks in total squashing timestamp bugs. We were integrating with a few existing systems that used epoch seconds or milliseconds for timestamps and they usually didn't have a hint in the field name to tell what it was so it was really easy to miss in code reviews.
Our problems were caused by a mix of serialization format (JSON numbers) and not always converting into the language's date/time types at the boundary (sometimes raw epoch seconds/millis were passed around layers of code and only parsed into a date for display. That created opportunities for misinterpretation at every function call.
My general rules for non-performance critical code are
1. Always Parse into a first-class date/time/duration type at the serialization boundary.
2. Always use an unambiguous format (e.g. ISO-8601) for serialization
It's not the most efficient but lets you rely on the type system for everything in your code and only deal with conversion at one place.
Durations aren't ints, so find or make a type suitable for storing them.
For a similar issue (cache durations that were expressed as a mix of milliseconds, seconds, and minutes) I now insist that the framework type `TimeSpan` is used instead for expressing these durations - as that's exactly what it was designed for.
One of the benefits of learning to program with Ada as the teaching language at University was the instinct to sub_type all the things. IF you're using a raw int you are probably doing it wrong.
> Type systems are to programmers what dimensional analysis is to physicists.
Dimensional analysis is to data in other domains what dimensional analysis is to data in the domain of physics.
Type systems are a tool in which dimensional analysis can be done, or, alternatively, which with which you can restrict data to be being dimensionally aware, allowing dimensional analysis to be done outside of the type system.
Reminds me of in C# how you can pass either an integer for time (usually in milliseconds) or pass in a TimeSpan object. I always use TimeSpan instead of an integer for exactly this reason.
Similarly, C++11 added a chrono::duration type. It's a massive improvement over the mess of C APIs which might expect anything from nanoseconds to whole seconds.
No, the credits could only be used to run ads. However, the ads were run publicly, and competed in ad auctions with campaigns from actual customers paying actual money.
If employees had a $25k ad budget, that would mean increasing the demand for ad placement by $25k, which could actually affect the ad campaigns of real customers - definitely not ideal.
Maybe we are misinterpreting the urgency of the messaging. I could just as easily read it as, "Hey guys having a problem with the pseudo-money credit. Shutdown until further notice."
Oh I’m sure there are unit tests for new func and old func that test them separately just fine. The if statement is probably thought of as a temporary switchover kind of thing.
Our test harness didn't catch it (weird combination of reasons, too long ago for me to remember the details) & it rolled out.
Shortly thereafter I get an anxious customer call that we'd charged their debit card $2500.00 instead of $25.00 and they'd gotten an overdraft notice. At first I was incredulous ("how is that even possible!?"), then I remembered that we'd just version bumped ActiveMerchant.
My endocrine response as I realized what must have happened was amazing to experience - the sinking feeling in my gut, hairs standing up, sweaty palms, dread, pupils dilating, and my internal video camera pulling back poltergeist-style in a brief out-of-body experience.
Fun times. Live and learn.