Twenty five thousand dollars of funny money
rachelbythebay.com
rachelbythebay.com
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.
That cinematic technique is called a "dolly zoom" and it's totally dramatic, if used correctly!
This is why I'm pretty dogmatic about variable comparison in tests.
This is dangerous and stuff like this has caused a lot bugs to slide though in my experience (and maybe ops):
expect(account1.balance).to eq account2.balance
This is safe and specifc: expect(account1.balance).to eq 2500
expect(account2.balance).to eq 2500
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.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.
Agreed on the ease of having problems of using variables on both sides.
Code that is write once read k < 10 times has very different lifecycle expectations than code that is constantly being work hardened.
var expectedAccountBalance = 2500
expect(account1.balance).to eq expectedAccountBalance
expect(account1.balance).to eq account2.balanceI 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.expect(account1.balance) eq 2500 before that isn't really a big problem.
Also "how to do it" is kinda different topic to "how to convince peers that's the right way"
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.
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.
1. "Reasonable" varies according to the particular situation (customer's order history, credit rating, the normal range of quantites for the product, etc.)
That doesn't catch the problem the moment it happens, but most times that's sufficient to catch it before anything hits production.
Seeing functions with 5-6 positional arguments makes my skin crawl even if they have strong types.
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.
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.
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
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
`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 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.)
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.
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.
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.
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.
[0] https://nim-lang.org/docs/manual.html#distinct-type-modeling...
(Unchained seems maybe the most featureful of those units packages.)
typealias Cent = Int
and then use a type Cent just like you can an Int, with multiplication and everything else. More info: https://kotlinlang.org/docs/type-aliases.htmlAlso, cents * cents shouldn’t be cents (though cents * unitless ints should be).
typealias Cent = Int
typealias Euro = Int
SomeEuro = SomeCent
In f#, measures aren't bound to any specific numeric type either.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.”
function foo(r: RobotName) {}
?
type RobotUuid = string;
const bar: RobotUuid = “abc…”;
foo(bar);
This is what duck typing is and specifically my curiosity about being able to de-duck on demand.
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.
assert istype(whatever, MyCustomType)
Which would throw an exception if whatever is not a "MyCustomType" at runtime.
Yes, and that’s roughly what TypeScript is: a linter that everyone on your team is running.
interface RobotName {
value: string;
type: "RobotName";
}
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.or some variation. The marker does not actually need to exist.
Here there is an interesting article about doing this in typescript (not affiliated)
It felt like a lot of busywork, but it did prevent some classes of bugs.
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).
The introduction of the additional types everywhere did turn into massive headaches based around dependency management and versioning.
So overall, I was personally disappointed in the results, but nevertheless happy to work with less "icky" feeling code.
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.
declare const isRobotID: unique symbol;
type RobotID = number & { [isRobotID]: true };
and now you can cast a number to a RobotID and back.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`.
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.
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.
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?
So yeah I’m curious how that worked too.
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.
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.
1 https://jsomers.net/blog/gettiers
2 https://www.eecg.utoronto.ca/~yuan/papers/failure_analysis_o...
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.
Debugging this was harddd.
They later switched this around, luckily I read the changelog.
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.
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).
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.
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.01999999Go has at least 25 community decimal packages[2], but who knows which is a good one to use.
[1] https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
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.
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.)
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.
https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
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.
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.
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.
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.
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.
Have unit tests!!
Could you do other things with those credits, like use them in the cafeteria or resell them? Why were they so frantic to close the loophole?
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.
Have unit tests and integration tests!!
:)
It is code that deals with $'s... something you'd really want to test, since it'll cost the company money.
Instead, you've got multiple engineers writing code multiple times (my euphemism for fixing buggy code), which also costs the company money.