8 ms·
An overview of the science on function length
- zelphirkalt 6y agoThe heading is sort of misleading. > To put it plainly, if we have a long function and split it into smaller ones, we’re not removing a source of defects, but we would simply be switching from one to another. Of course ... But when you use short functions, you try to abstract things and make these functions usable in general cases. Who thinks, that simply splitting up a long function with zero changes fixes any bugs? Hopefully very few people. The only advantage in that case would be, that one can think in the context of that smaller function about what it is doing and that might help to find a bug _and then fix it_ by changing the logic. By then we have done already more than simply splitting up the long function though. Long functions usually do more than one thing to achieve a more complex thing, which requires doing multiple steps. That makes them less reusable. They become too specific in what they achieve. If ones naming skills (those are very important) are not up to the task of naming long functions precisely, then perhaps one will forget, that this long function does something very specific inside to reach its goal. It will silently have lost composability. Other developers for sure will not know, unless they read all the code. I have seen atrocious 300+ loc functions, which serve exactly one purpose and interact with so many parts of the system, that I can read them 10 times and still do not get what they do in their entirety. By not using small functions, one gives away readability as well. Every function is a chance to give a name to some short program. Short functions can be looked at separately, if written well. Of course, if you modify a lot of global state in your short function then no one can help you any longer. > All of the studies measuring defect density found increased defect density for smaller functions. One possible explanation was suggested by [2], who proposed that the increase in errors was due to what they called “interface errors” – that is, “those that were associated with structures existing outside the module’s local environment but which the module used.” This would include errors such as calling the wrong function. Well, that happens usually, when the naming is off or difficult to remember or follow. That is why naming things is an incredibly important skill. The name of a function should, if possible, give a good picture (by convention if there is any) of what the function is doing. Do not tell me, that the 100 loc function can be described in a single verb. It is most likely doing much more than one thing. So in general I am not convinced by how the title and intro of the article put it. If we look at the article, the title is also actually different than here. It says "Very short [...]" not "short". They would have to limit the scope of their study a lot, to make a good point. For example: (1) procedural code / mainstream "every noun is a class" code / functional code (2) the kind of expertise of coding people allowed to take part (Was the code written by capable people?) (3) the programming languages they look at It seems that this research might be biased by what code they looked at. Some programming communities are not as well known for keeping code clean as others. They are often associated with "only learned one programming language ever, never widened their horizon to a different methodology or paradigm". Those are mainly found in languages, which are very widely used and up at the top of the programming language usage lists like TIOBE. The reason is, that being at the top of the list is used by many as a justification to not needing to learn a different tool. Many people stop learning. Here I quote the article: > However, nowadays the vast majority of functions are under 50 lines. A quick analysis of Eclipse, a popular open source IDE, reveals it averages about 8.6 lines per method in its source code. Relating to an IDE, which is mainly used for Java. > This shift in function sizes is perhaps partially due to changes in programming languages. In the 80s a Fortran “module” was commonly considered a function and some variables (see eg. https://www.tutorialspoint.com/fortran/fortran_modules.htm https://www.tutorialspoint.com/fortran/fortran_modules.htm) and function was the basic building block of software, whereas nowadays most Java or C++ programmers would define “module” as a class consisting of multiple functions. They do that, because for a long time their languages were so limited, that they did not have any other means of expressing a module. This also discouraged a style which does not use objects, but only functions. Add to that, that Java (and I think neither C++) does not have TCO and you have yet another reason, why Java or C++ code is not a good argument, when talking about short functions. Java got modules now, as far as I know. Not sure developers make proper use of them. I would like to see the same research being done on a language like Haskell or Scheme, one language at a time.
- tziki 6y ago>So in general I am not convinced by how the title and intro of the article put it. If we look at the article, the title is also actually different than here. It says "Very short [...]" not "short". The "very" word was automatically stripped by hackernews. I'm guessing some system to tune down clickbait posts? >By not using small functions, one gives away readability as well. Every function is a chance to give a name to some short program. Short functions can be looked at separately, if written well. Of course, if you modify a lot of global state in your short function then no one can help you any longer. I disagree about readability - I think the empirical experiments show enough evidence about short functions not being more readable than long functions, that we can't take that as granted. For reusability, you might be right, and maybe that's why short functions did seem to perform better when modifying existing functionality. >Well, that happens usually, when the naming is off or difficult to remember or follow. That is why naming things is an incredibly important skill. The name of a function should, if possible, give a good picture (by convention if there is any) of what the function is doing. Do not tell me, that the 100 loc function can be described in a single verb. It is most likely doing much more than one thing. Sure, but naming things well is also a difficult skill, which means not everyone can do it well. If we look at programming as a worldwide phenomenon, the vast majority of programming is done in English. However, the majority of programmers aren't native speakers, meaning they face an even steeper climb to become good at naming things. There will always be cases where programmers misname functions, or don't update the name when changing functionality, or typo, and I think the data here shows that things like that do happen. >I would like to see the same research being done on a language like Haskell or Scheme, one language at a time. I do agree that all the research focuses on mainstream languages, and the results might be different for functional languages. However, the point of this post is to look at science and there's simply no science there. There aren't any bug datasets focusing on functional languages either. The point of this post is to gather what we know about function length and what we don't. I feel you've pointed at many of the areas we don't know much about - and maybe we'll get there one day. However, for now, we need to look at what we know and what kind of generalizations we can make from what we currently know.
- jghn 6y ago
- neilwilson 6y agoDoes anybody do coupling and cohesion any more? That was always the dividing line I was taught. A cohesive function you could name to increase the abstraction level and stop your brain overloading.
- brundolf 6y agoSomething I didn't appreciate until several years into my career is that even decoupling has a cost. By adding a layer of abstraction, you're making a bet that the mental overhead of tracking that new idea is smaller than the mental overhead of tracking its implementation directly. You're introducing a new concept that the programmer didn't have to think about before, in hopes that it allows them to ignore a larger set of information some of the time. For tiny functions, especially if their concept isn't already familiar to the programmer, this may often not be a good tradeoff.
- roughly 6y agoI'm unconvinced. Two notes - first, the experimental evidence seems to involve taking people unfamiliar with the codebase and asking them to debug or investigate code with either long or short functions. I can certainly conceive of it being easier to learn and debug a single long function than multiple smaller ones if one is unfamiliar with the codebase. I'd expect that effect to go away once one is asked to debug the same code again. Second, the article asserts more bugs are found in shorter functions. However, this is only taking lines of code into account, not functional complexity. As code lives, bugs are found and corrected; often those bugs are to do with unforeseen runtime circumstances. Joel Spolsky covered this memorably a couple decades ago [1]. Again, taken as a static snapshot, I am not surprised that short code correlates with bugs, but I don't think that tells us any meaningful information about how to code. A codebase is not a static entity. As pointed out by several people already, the point of short functions is abstraction and comprehensibility - we want one and only one place to talk about reading a file, one and only one place to talk about handling a particular message type. Similarly, we want to know when we look at a function that it does one and only one thing, and we want to know when we look at our code that what it says it's doing is what it's actually doing. I think this is one of those studies similar to the "judges sentence harsher before lunch"[2] study - the type of effect discussed fails under any reasonable comparison to commonly-experienced reality. Consider your own personal experience working in codebases and how this article aligns with that - phenomenological reporting can indeed be suspect, but when it contrasts starkly with experimental results it often indicates poor study design. [1] https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/ https://www.joelonsoftware.com/2000/04/06/things-you-should-... [2] http://nautil.us/blog/impossibly-hungry-judges http://nautil.us/blog/impossibly-hungry-judges (I hope I can use this level of motivated reasoning to critique the next study I agree with, too.)
- eesmith 6y agoSeveral of the papers referenced, at least, use empirical evidence not based on the experimental procedure you described. Eg https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-2005-149.pdf https://www.microsoft.com/en-us/research/wp-content/uploads/... and https://www.inf.usi.ch/faculty/lanza/Downloads/DAmb2010c.pdf https://www.inf.usi.ch/faculty/lanza/Downloads/DAmb2010c.pdf . I think the author is not saying "more bugs are found in shorter functions" but that more defects are found in software modules with shorter functions. Hypothesized reasons for this include ""interface errors"" and ""it’s plausible that creating functions will always carry some [cognitive] overhead in itself"". Even then, the only firm conclusion is for very small functions, at most a few lines long. Are those the same sorts of short functions you mean? You write: "A codebase is not a static entity." And that's true. Which is why the cited paper https://www.inf.usi.ch/faculty/lanza/Downloads/DAmb2010c.pdf https://www.inf.usi.ch/faculty/lanza/Downloads/DAmb2010c.pdf (for example) tracks repository changes for several open-source projects for several years, and correlates them with reported defects. That was used in the essay, eg, ""this is likely because we consider all bugs over the ~4 year period"". My own history is that working with code bases which follow the 'functions should be only a few lines long' approach drive me batty because I can't remember all those function names which are only used once, and because many of those code bases treat instance methods as pseudo-global variables, and modify them will-nilly. So yes, the conclusions given here agree with my personal experience, so suggest that it is a good summary of research which was reasonably well-defined. Which is a rather different conclusion that yours, yes?
- gumby 6y agoI lop Ike this author: > Time to scare the wikipedia editors among you and do some original research. All the code and data can be found at...
- awinter-py 6y agoHmmm I wonder if test coverage is a factor here One long function gets run every time you change any piece of it, whereas a more complex call graph can be edited without testing every permutation (yes there can be branches in the function too)
- aaron-santos 6y agoIs it difficult to believe that functions can be "too small"? I've found myself in both code bases with enormous 5kloc functions as well as code bases where I had to juggle 10 1loc functions in my head in order to figure out what the hell was going on. Both are their own forms of torture. A 5loc/function sweetspot sounds about right.
- christophilus 6y agoExtremes are generally suboptimal, but the prevailing wisdom seems to be to have many small functions, so I’m all for a study that pushes us back to the middle.
- tsimionescu 6y agoI think that this result is rather intuitive, even in the common paradigm of 'a function should do one thing and have a good name that describes that thing'. Essentially, the idea that code is easier to understand with small, well-named, cohesive functions relies on the functions being bug-free themselves, and on the relevant behavioral details being perfectly caught in the name and types. However, if small functions have bugs, it is pretty intuitive that it takes more time to explore a call graph than a linear code listing to find that bug. Furthermore, when chaining many small functions to achieve a more complex functionality, bugs can more easily slip in the chain itself. For example, a function may modify a list you pass in and also return it, when your chain assumed it would return a copy. A sort function may be unstable when the calling code assumed it was stable. A function may move to another thread when the calling code assumes that locking is unnecessary. All of these would be easier to catch if the code were inline, instead of abstracted behind a function signature. Of course, this has to balance somehow with not rewriting sorting procedures in every function of your code base. I think that the answer probably lies with some idea of separating library code and application code. Library code should be composed of many small functions, well documented and well unit-tested. Application programmers should all be familiar with the library and its semantics. In contrast, application code should probably favor larger functions and avoid ad-hoc abstractions. If there is a small piece of functionality that seems re-usable, it should usually not be moved to a separate function, but to a separate library.
- leto_ii 6y agoI'm not sure the first part of the article is based on a sound methodology. For example, we don't have the number of classes for each bucket in the plots [1] and [2]; perhaps there are only a few classes of one liners - probably not a statistically representative sample. It's also not clear what type of code is counted. I'm not convinced boilerplate (setters, getters, simple constructors) should be taken into account. It will artificially decrease the average method length in a class. Imagine a class with 10 fields (hence 10 setters, 10 getters, maybe a few constructors) and one method of 100 lines. The average method length will be 120 (100 + 20*1 from the boilerplate) / 21 ~= 6 lines of code. If there are bugs in this class, they basically have to be in the large method; still, in statistics this would count as a class with pretty short methods overall. I'm also surprised by what counts as a long method - in my experience code starts getting messy with lengths > 20 - 30. These methods don't even seem to be represented in the data. > application code should probably favor larger functions and avoid ad-hoc abstractions Why favor large functions? Maybe allow them, but why encourage? Also, we haven't established what large is (20 lines - perfectly fine; 200 lines - I don't think this can ever be justified). > If there is a small piece of functionality that seems re-usable, it should usually not be moved to a separate function, but to a separate library. This does seem excessive. Don't you think there might be code that can be locally reused? Does every little thing have to go to a library? [1] https://i2.wp.com/softwarebyscience.com/wp-content/uploads/2020/08/Screen-Shot-2020-08-24-at-9.13.53-PM.png?resize=768%2C564&ssl=1 https://i2.wp.com/softwarebyscience.com/wp-content/uploads/2... [2] https://i1.wp.com/softwarebyscience.com/wp-content/uploads/2020/08/Screen-Shot-2020-08-24-at-9.15.45-PM.png?resize=768%2C563&ssl=1 https://i1.wp.com/softwarebyscience.com/wp-content/uploads/2...
- ajmurmann 6y agoI saw this discussed elsewhere where it was pointed out that Java has a tendency to have objects with lots of small getters and setters. This might effectively be a proxy metric for objects that do too much and expose too much of their state.
- cjfd 6y agoWhen there is a correlation between A and B it could be that A causes B or that B causes A or that A and B have a common cause. It could also be that the correlation is just a coincidence. I tend to write longer functions when the thing being done is simple and shorter ones when the thing being done is complex.....
- outsomnia 6y agoIt's true one huge function repeating things and excessive fragmentation into subfunctions are both less than ideal to maintain and there's a desirable middle ground. But >> Very short functions are a code smell that's definitely not unconditionally correct. There are many cases where dereference helpers of the form x_to_y(x), wrapping type conversions that might go through three or four levels of struct members are going to eliminate mistakes in bulk code. They may be done as preprocessor defines or as inline functions but either way they are usually a green flag, not a red flag.
- eesmith 6y agoYou quoted the title, but the title isn't the complete conclusion, which is: > As such, software developers should be wary of breaking their code into too small pieces, and actively avoid introducing very short (1-3 lines) functions when given the choice. At the very least unnecessary single-line functions (ie. excluding getters, setters etc.) should be all but banned. I believe a x_to_y(x) fits into the "etc." of acceptable single-line functions.
- sgk284 6y agoThere are so many compounding factors here that the conclusion seems unreliable. It also reminds me a lot of the streetlight effect[1], in that defects may simply be easier to detect in smaller functions so that's where we find them. Many of the conclusions center around density and lines of code. This is weird because if smaller functions lead to less overall code, then if you have 2 bugs in a 1,000 sloc codebase it'll measure worse than 6 bugs in a 10,000 sloc codebase that does the exact same thing. It might be more valuable to compute the defect rate per cyclomatic or kolmogorov complexity. Or some other measure that's independent of line length and then figure out how function size impacts those complexity measures. [1] https://en.wikipedia.org/wiki/Streetlight_effect https://en.wikipedia.org/wiki/Streetlight_effect
- eesmith 6y agoWhat do you consider to be the conclusion(s)? I ask because the conclusion is only firm about very short functions, otherwise saying "[f]or longer functions, the picture is less clear" - which you agree with, yes? Part of the conclusion concerns how short functions appear to be harder to debug, which seems rather the opposite of your hypothesis that defects are easier to find in them. (One of the contributing arguments is that the indirection of a function name adds its own mental overhead.) Nor do I see a conclusion that smaller functions lead to less overall code. The article seems more concerned about questions like "should [we] refactor our long methods into short ones to avoid defects", with that study concluding no. And the article addresses code complexity several times, most directly as: """The literature doesn’t provide a straightforward way to measure which feature (length or number of methods) is more significant when predicting defects. It’s quite possible that, for example, measures such as “average method complexity” and “number of methods in a class” simply act as 2nd order estimators for number of lines, in which case we’d simply be comparing which measure correlates better with the underlying metric.""" I don't see how your answer fits in, so did I misread the article that badly?
- leto_ii 6y agoI think you make a valuable point. If somebody writes concise, clear code with little duplication, but still has a bug, their stats might look worse simply because their code is shorter. It's also not clear that bug rates should be the only/main thing to look at. Complexity measures can also help to indicate if the code is easy to read/extend. In my experience low cyclomatic complexity does correlate with more readable code.
- asgard1024 6y agoI am glad that somebody is trying to challenge the conventional wisdom. This might also be relevant: http://number-none.com/blow/john_carmack_on_inlined_code.html http://number-none.com/blow/john_carmack_on_inlined_code.htm...
- freetime2 6y agoI find that a lot of junior developers tend to go overboard with creating small functions, and the motivation is largely aesthetic. They have a mental image of what “clean” code is supposed to look like, and it doesn’t involve lots of indentation, curly braces, mathematical notation, etc. So they often try to bury these messy details inside a function. They don’t have a lot of experience reading and debugging other peoples’ code, so they might not consider how much of a pain it can be to have to go jumping through a whole bunch of deeply nested one or two-liner functions just to see what the code actually does.
- CuriouslyC 6y agoSmall functions are a form of semantic annotation when used properly. If I'm working with code that modifies an image, I'd much rather see "rotate" than a matrix multiply. Heck, if I'm dividing a number by some special value, say to normalize it, I'd prefer to see "normalize" than a mystery division. I do agree that I'd rather see x + 1 than "addOne" though, since that doesn't tell me anything the code itself didn't.
- freetime2 6y agoIf a block of code is short (a few lines or less), and doesn't get reused in multiple places, I often prefer to add a clarifying comment rather than extracting the code out into a separate function. That way they reader doesn't need to go jumping around to see what the code does. And you can fit more information on a comment line than you can in aSuperDuperDescriptiveFunctionName.
- klyrs 6y ago> "... if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program." - Linus Torvalds I've had that quote thrown at me by a petty reviewer. Okay advice in C, utter shitshow in python classes. Like pep8 and Dijkstra's rant on goto, it's generic advice, not a holy mandate.
- besulzbach 6y ago
- jake_morrison 6y agoThis post describes a process of decomposing code into very simple functions: http://www.gar1t.com/blog/solving-embarrassingly-obvious-problems-in-erlang.html http://www.gar1t.com/blog/solving-embarrassingly-obvious-pro... This results in functions which are, I feel, a bit too small for comfort. On the other hand, Erlang's pattern matching on function parameters has an effect similar to Eiffel design by contract. It has the potential to reduce the number of tests that need to be written (many of which are often very short functions). Ideomatic Erlang/Elixir code tends toward smaller functions and pattern matching on multiple function heads instead of if/then/else logic.
- tcldr 6y agoBy this measure, Haskell is an abomination and Combinators are a 'code smell'. Not sure I agree.