5 ms·
I completely miss the point of the article, but my pet peeve is that operator== is even defined for floats in most programming languages. It really, really shou
by ajuc 4y ago
I completely miss the point of the article, but my pet peeve is that operator== is even defined for floats in most programming languages. It really, really shouldn't.
Instead if should return an error "Floating points should be compared using this library function that includes acceptable difference. If you want exact math use BigDecimal or similar. If you know what you're doing use library function with acceptable difference = 0.0"
And yes, Uncle Bob is giving some terrible advice. My least favorite is his advice to split "too long" pure functions into a stateful class with "short enough" methods that later can be called in the wrong order because now there's a right and wrong way to call them.
- Joker_vD 4y agoOh, but one generally wants an "acceptable difference" only when the numbers compared are close enough to zero, otherwise the "acceptable relative difference" should be used instead, right?
- ajuc 4y agoYes, it's more complicated than that, but at least "straightforward" code with x==y won't compile so you'll have to think about it.
- seabrookmx 4y ago> can later be called in the wrong order I'm thankful I'm not the only one with this critique. I see this so often and it makes navigating and/or debugging the code a nightmare, while I hop around these tiny single use methods. A long function I can read top to bottom is a lot easier to grok!
- forgetfulness 4y agoExamples of that were common in 2000s poor Object Oriented Programming introductory material, do people still advise that kind of design? There were people who abhorred the sight of non-stateful code, there was little opportunity to fiddle with objects in it, not realizing that they just made their and everyone else's job harder with more moving parts, a concept that, despite being very easy to understand and with analogues to physical machines, was lost on many programmers.
- johannes1234321 4y ago> do people still advise that kind of design? It is a constant discussion. You can always find examples of long functions with lots of state which is hard to follow and then there are always examples of code where people went too short. And then there are always people taking any of those examples and showing why they were wrong. I myself like going to the extremes for fun/toy/throwaway projects (not only on these but also with rules like "no `if`" or "no raw `for` loop" etc) to try out all the different alternatives and then resetting my default based on cases where it went well and where it went bad. No such rule is always right, but most hmstem from being exposed to too much of one extreme.
- layer8 4y agoThe correct way to do this, if it really is a significant simplification, is to factor out the minimal relevant code into a private helper class (or struct) that you instantiate anew for each computation. It then functions like a stack frame you can pass around and share between functions/methods, within private boundaries.
- BurningFrog 4y agoI used to call this the "Poltergeist Pattern". Tons of tiny functions, each doing almost nothing, frantically calling each other, and somehow useful functionality results. Each function is very easy to understand, but how they together do useful tasks is near incomprehensible.
- camgunz 4y agoI had a gig where a consultant had just been through (before I started), and totally changed the programming culture of the engineering teams. One of the weird things that happened was my new team no longer tolerated methods > 10 lines, which created exactly what you're talking about: a bunch of tiny single-use methods and helpers scattered across other classes/files/modules/libraries that totally obscured control flow. It's amazing to me how even smart people can be taken in by Harold Hill types. Can I get a job going around giving advice for $100k a pop? I promise to do better than that guy haha.
- tziki 4y agoThat's funny, I wonder how nobody thought to challenge that.
- ratww 4y agoIn a team I was part of, we used the default in a popular Ruby linter, Reek. And it wasn't 10, it was 5. The team treated that as puzzle-solving, and it was part of their culture to tackle those kinds of challenges. There was light hazing of newbies whenever the CI didn't pass because methods were too big. So, that's why at least in this case. There was no technical argument, just inertia. Code quality was atrocious. Things that could be 10 lines were 30 or 40, and extremely stateful.
- btschaegg 4y agoOn that topic: I came to like John Ousterhout's notion (in [1]) of "deep" methods/classes/etc. He explains it like this: If you have a reusable piece of code, imagine drawing it as a rectangle. The width corresponds to the complexity of the interface, the depth corresponds to the complexity of the implementation. Ousterhout is saying that we really want deep classes, i.e. the complexity of the implementation a construct hides should be considerably larger than the complexity it takes to use it. Which, at least to me, basically says "search for good abstractions". And what you're describing strikes me as a good way not to achieve that. Of course, there's considerable wiggle room in that mental model, and one could still endlessly squabble about how (not) to break up the implementation. [1]: https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201 https://www.amazon.com/Philosophy-Software-Design-John-Ouste...
- im_down_w_otp 4y agoIs that last paragraph actually a thing? Yikes! That sounds like the kind of advice one would give if they've only ever dealt with toy problems.
- ajuc 4y agoIt's one of his solutions for refactoring long methods that operate on more than one variable, so that simple "extract method" refactoring isn't feasible. He says to turn the function into a class and local variables into private fields. So that you can split the function into small methods that operate on these private fields. I used it a few times in a company that had strict "no Sonar warnings" policy. But I hate the resulting code much more than the initial code.
- ssrc 4y agoThe jack-in-the-box (or hellraiser's box) style of OOP. You call a succession of 0 or 1 argument methods that change the object state until suddenly something happens.
- forgetfulness 4y agoFloats can be compared for equality, they're a pattern of bits in memory after all. operator== can be defined for floats. Using it on the results of floating point arithmetic may be a bad idea, but that's a different matter. That should perhaps be a compiler or linter warning, rather than flat out be declared incorrect.
- marginalia_nu 4y agoFloats can be subnormal, and different bit patterns can represent the same value. It's not as easy as just comparing bits. Here is some cursed C code to play with: #include <stdio.h> #include <float.h> #include <math.h> int main() { int a = 0x1000002; int b = 0x2000001; float* fa = (float*)&a; float* fb = (float*)&b; printf("value-equals? %d\n", *fa == *fb); printf("fabs-equals? %d\n", fabs(*fa-*fb)<FLT_EPSILON); } May print something like value-equals? 0 fabs-equals? 1
- jbay808 4y agoI'm not sure how subnormal numbers are relevant. For binary floats, the only duplicate values are (+0, -0) and the NaNs, and in both of those cases it's really up to you to decide if those are really representing the same value or not.
- marginalia_nu 4y agoI'm arguing against the notion that floats are just bit patterns in memory. If you base your operator== on such a notion, you will get very weird behavior.
- eesmith 4y agoCould you explain why that output is cursed? I get the same output using the following (I switched to the portable memcpy solution.) #include <stdio.h> #include <float.h> #include <math.h> int main() { int a = 0x361dc5df; // 2.351e-6f int b = 0x361e3e22; // 2.358e-6f; float fa, fb; memcpy(&fa, &a, 4); memcpy(&fb, &b, 4); printf("value-equals? %d\n", fa == fb); printf("fabs-equals? %d\n", fabs(fa-fb)<FLT_EPSILON); } Since FLT_EPSILON is 1.19209290E-07F , I would expect a lot of small, close numbers to give the same output, even without touching subnormal values.
- marginalia_nu 4y agoA lot of Uncle Bob's advice come from a time and a place where they actually had merit. There is a sort of enterprisey Java code that was very common in the '90s. OOP and Gang of Four was incredibly trendy, but few people were actually all that good at it. So now your code had a AbstractFactoryFacadeDelegatorImplBuilderVisitorVisitor. Methods were often extremely long, hundreds of lines. The code was complex in a lot of ways, lots of static instances being accessed from every which way. In this context, Uncle Bob's style was compelling, it felt like a breath of fresh air. His opinionated attitude was also compelling. In general, associating a term like 'clean' to your methodology seems to be a good zealot recruitment strategy. 'Pure functions' is similarly brilliant marketing. That said, a lot of his supposed solutions actually cause problems of their own if you let the pendulum swing too far. It creates its own kind of complexity. Books like these also tend to sort of get their own momentum, if you look back you see a lot of big names praising them. Surely they must be good, right? Well they were. Clean Code made sense when it was published. It's for the most part not good advice today.
- mypalmike 4y agoI haven't seen anything much better than SOLID from Clean *Architecture in terms of organizing large projects into lots of small discrete interfaces. I guess globally declared pure functions are being pushed a lot today, but I find this approach organizationally wanting, particularly in promoting tight coupling (with all its drawbacks). *Edit: Updated: I had forgotten this was not from "Clean Code".
- ajuc 4y ago> I guess globally declared pure functions They don't have to (and probably shouldn't) be globally defined.
- mypalmike 4y agoI meant "globally declared" in contrast to instance-oriented (i.e. methods) rather than whether or not a function exists within a namespace, which is what I think you're implying.
- aaaaaaaaaaab 4y agoInteger floats can be checked for equality perfectly. Uncle Bob is a fraud by the way. Turning a perfectly readable medium length function into a class with dozens of tiny methods is a cardinal sin in my eyes. Then later someone stumbles upon this class and reuses some of these methods in one way or another, while working on a problem completely unrelated to the original, thus entangling the two implementations…
- pmcollins 4y agoi like the idea that uncle bob subscribes to and teaches a certain style or school of coding. kind of like judo vs karate etc.. you may not be an adherent of judo and you may prefer and better understand karate, but that doesn’t make a judo teacher a fraud.
- ajuc 4y agoInteresting. In martial arts world there were lots of styles that claimed to be "the best" until they did UFC and suddenly people discovered that if you don't know brazilian jiu jitsu you will most likely lose to someone who knows it. What's the programming analog of UFC/MMA?
- jgon 4y agoThis analogy doesn't work because programming in "industry" has been like the MMA since day 1 in the sense that you have always had to "test your skills" and make something that people wanted and compete with other products. In the martial arts world a bunch of different martial arts just completely went without full-contact sparring/competition and instead built up a bunch of different rules and scenarios around how they were "too deadly to be done in practice". This is the bullshit that MMA exposed, and its interesting to note that the two practices in the comment you're responding to, judo and karate, have had a long history of being practiced "for real" in the gym and in competition and thus have spawned a long line of highly successful MMA competitors. The analogy also doesn't work because BJJ isn't some silver bullet. What people discovered is that the first M in MMA is actually the important part and if all you know is BJJ you're going to get starched by a boxer with a sprawl, or more likely a wrestler with a modicum of submission knowledge, who will never let you get to the floor in the first place, and instead just grind you out. So just like the question "what is the best martial art" currently has no answer outside of "you need a mix of striking and grappling not just one thing", there is no answer to "What is the best programming style" outside of "think about the problem you have at hand and crib on examples and knowledge from other people who have solved a similar problem". This "unfortunately" points to boring industry standard tools, like Java, C/C++, Javascript, RDBMSs, IDEs, Linux etc, etc. Probably some newer stuff like Rust and React as well. And note that answer isn't one specific technology, like MMA its a bag of different tools you combine.
- zoomablemind 4y agoMemory based == is not unreasonable for floats, once in exact representation domain. Of course, if operating in approximated domain, especially involving any of existing math libraries, the float compare should be done with corresp. functions/macro and be epsilon based.
- lgleason 4y agoDo you practice TDD? That is usually where the long methods start to become an issue.
- ajuc 4y agoUnit testing pure functions is a pleasure, no matter if they have local variables. assertEquals(expectedOutput, f(input)); When you split them in a way that makes the local state external - you now have to handle the state in unit tests which is much more hassle.
- lzsiga 4y agoOff: floating point numbers can be used to store integer values, so equality comparison might be perfectly valid in some cases. For example, if the embedded Sql doesn't support 'int64_t' (only int32_t), itt might still support 'double' which can store 52-bit integers exactly.
- ajuc 4y agoYes, also there are deep magic gamedev tricks exploiting that (see quake sqrt :) ). There should be a way to do them. Something like bit_compare(x, y). I'd just prefer if rarely useful operations shouldn't use the most commonly used API. Too easy to make a mistake. BTW I'd also love to have a built in float type that fails when you assign 0 to it. Anyway, I guess in Java operator == is a lost cause anyway. My favorite example: // true System.out.println(BigDecimal.valueOf(10) == BigDecimal.valueOf(10)); // false System.out.println(BigDecimal.valueOf(11) == BigDecimal.valueOf(11)); // true System.out.println(BigDecimal.valueOf(11).equals(BigDecimal.valueOf(11)));
- Gibbon1 4y agoI think if you want to perform binary operations on floats you should have to cast them to a binary type.
- lelanthran 4y ago> Off: floating point numbers can be used to store integer values, so equality comparison might be perfectly valid in some cases. Yeah, but then you're having to learn all the special cases for when it silently gives wrong answers, and hope to hell that you didn't miss any. Much better to have consistency and behave the same way all the time, than to optimise for 3 keystrokes and introduce all sorts of special exceptions that the programmer must memorise.
- jacquesm 4y agoThe 'clean' pattern to me is a complex data structure that is operated on atomically by relatively simple functions, but not so simple that you need to keep a 6 level call stack in your head to follow what is going on. Function decomposition is fine as long as - like everything else, really - you don't overdo it.
- layer8 4y ago> my pet peeve is that operator== is even defined for floats Then people would start writing `signum(a - b) == 0` (or some equivalent) instead of `a == b`, and/or factor that out into a helper function. Not sure that would be an improvement.
- Gordonjcp 4y ago> my pet peeve is that operator== is even defined for floats I fixed two bugs at once in a softsynth caused by this stupidity. In the envelope generator code, which loosely modelled the ADSR circuit in a synth which charges and discharges a capacitor, there was an integrator implemented like `env = ((target - env) * timeconstant) + env`. The `target` value was set to 1 to allow the "capacitor" to "charge" at a rate set by `timeconstant`, and when it reached the top it would be flipped to 0 and `timeconstant` changed to the decay rate. It failed about one go in ten, with some fudges to set `timeconstant` in odd values. It turns out that they detected the "capacitor fully charged" state with something like `if (env == 0.999) { dostuff(); }` and for some values of `env` and `timeconstant` it might never hit *exactly* 0.999 if you got it exactly wrong. It was logically wrong (detect a precise float value) and functionally wrong (envelopes switch over when the cap is about 2/3 charged). Changing the "flip over" condition to something more like `if (env > 0.999)` and changing the maximum value of `target` to 1.5 (mimicking a circuit running off 15V and trying to it a 10V output, like a real analogue synth) cured all its problems. Well, all its *envelope* problems. There were more, but they get into scary discussions of poles and zeroes and negative frequencies, and you don't want that at this time of night.