4 ms·
Interestingly, I had the exact opposite reaction. I found the second function harder to reason about because it heavily relies on mutation and the fact that ind
by klauserc 3y ago
Interestingly, I had the exact opposite reaction. I found the second function harder to reason about because it heavily relies on mutation and the fact that individual loop iterations commute with respect to one another. (To understand what the final value of `min_value` is, you first have to understand that `<` being transitive means that the iteration order doesn't matter)
The first function, on the other hand, has only static assignments (one name is only ever bound to a single "definition" at any one time). In terms of working memory, that means that each variable in the first function only occupies a single "slot". In the second function, the mutable `min_value` variable necessarily occupies multiple "slots", one for each point in the program where the variable could change. So you'd have to keep track of a "min_value[0] = 9999", a "min_value[1] = number (if number odd && < min_value)" and _maybe_ a "min_value[2] = min_value (if number even || > min_value)".
- JackMorgan 3y agoI agree, I read the first sample almost instantly, and I'm only _pretty sure_ I follow the second one after staring at it for a minute. Changing it to a range+filter+fold would make it an order of magnitude easier to read for me. I do agree with the rest of the article. It's the same stuff I've been doing since reading Clean Code over a decade ago. This gets me thinking, there really is an opportunity cost associated with spending a lot of time working with sequence abstractions and immutable data structures. The better I get at reading immutable code, the harder it is to read code with mutation.
- Scarblac 3y agoIt depends on how often you have written loops just like that though. To my eye, setting a min_value, then looping through the numbers updating min_value when something lower is found and returning the end result is just a single "chunk" that's immediately obvious. With the highly unusual addition that there's an extra if statement inside. That's weird. And the initial value of 9999 is highly suspicious. All the numbers may be higher than that. I'd say it has complexity value 4 -- "find the minimum value, in the usual way, except only look at odd numbers, and use 9999 as the initial value".
- deleted 3y ago[deleted]
- AnimalMuppet 3y agoGeneralizing from your first paragraph, things occupy only one chunk when they're an idiom that you know well. That you know well. This is why there's so much disagreement on this stuff. Different people have different idioms that they know well. You're never going to be able to reduce "what is simple" to something everyone agrees on. There's an actionable part of this observation. If you're responsible for a code base, it should use a limited number of common idioms. Have everyone working on the code base learn and use those idioms in preference to other approaches. Consistency reduces the mental burden on the reader.
- larksimian 3y agoI just had the same sort of realization actually, trying to explain to myself why I dislike micro-functions: it pollutes the vocabulary that I need to know at all times with super low value information words that are only relevant in very particular contexts(and that I don't necessarily trust to not be mislabelled). This is an unappreciated value of using code frameworks and doing things the 'framework' way. You might have a somewhat convoluted function using framework or std lib functions to do a task but anyone familiar with the tool can just read it top to bottom and have a genuinely deep understanding of what the code those and what edge cases might show up. If you convert that into your own function vocabulary by wrapping the 'basic' code into extra methods... nobody can read the superficially more elegant function and know anything more than what the function names are telling them. It's like they're reading pseudo-code and can only guess at the implementation subtleties unless they annoyingly 'manually' inline the code by jumping to and reading through the custom function definitions.
- amelius 3y agoYes. The second example would be much simpler if they wrote it in a more functional-programming way. E.g. by first filtering for the odd numbers, then calling a minimum function.
- larksimian 3y agoI agreed with their assessment though for a totally different reason: the second function is only doing primitive operations and not invoking any other functions. For the same reason I found that their refactoring actually made the first function worse. Now I have even more jumps that I need to make in order to 'inline' all the code so that I can deeply understand what the function is doing. There seems to be an implicit assumption that you should just trust the names of the functions and not look at their implementation. This is hopelessly naive. In the real world, functions don't always have obvious names, their implementation can involve subtleties that 'leak' into usage etc. I strongly dislike function extraction that's driven by anything except the need to reuse the code block. Function extraction for readability is about as useful as leaving a comment above the code block. Honestly I'm more likely to update the comment than the function name since changing the fn name means updating the callers as well. edit. To add a bit of nuance: I think 'primitive' vocabulary is what's essential here. The standard lib of a language is primitives. The standard functions of a framework like Ruby on Rails are primitive. I'm happy to see code written by calling functions/types/operations that I have seen before dozens or hundreds of times. What I don't like is when a feature in a codebase has created it's own deep stack of function calls composed out of functions that I know nothing about. Create a rich base vocabulary that is as widely shared as possible and use that for your work. Avoid creating your own words as much as possible. This way I can glance at your code and not just see if (BLACK_BOX_1 or WHAT_DO_I_DO_AM_I_LYING) then DO_SOMETHING_BUT_MAYBE_I_ALSO_DO_SOMETHING_ELSE_WHO_KNOWS
- AnimalMuppet 3y agoIf I have a function call to WHAT_DO_I_DO_AM_I_LYING, that's no better than a block of code with a comment that says "WHAT_DO_I_DO_AM_I_LYING". The difference is that, once I look carefully at the code and find out what the called function does and that the name (optimistically) isn't lying, then the called function only takes up one line in the code I'm looking at, whereas the in-line block takes up several lines (plus the line for the comment). For me at least, the function call takes less mental space (if the function name is accurate).
- larksimian 3y ago
- drumttocs8 3y agoI'm a controls/automation engineer, not a SWE, and the first function is infinitely easier to understand for me. It simply says what it's doing, in order of operation, in easy to read code. I don't know, does that mean it's declarative? I don't understand how that's supposed to be more difficult than stepping through loops.