Catalog of Elixir-specific code smells
github.com
github.com
Quickly glancing through this, I see the Complex Branching case as an example (https://github.com/lucasvegi/Elixir-Code-Smells#complex-bran...). It makes my brain need to act like the runtime, storing the stack of the current function in my head, jumping to the function call, then returning back to the original function and restoring the stack (to be clear, I'm not talking about the actual execution, I'm talking about what my brain goes through reading this).
In a complex codebase, the jumping can be far (physically and figuratively), especially since there's a strong preference for pattern matching in function parameters (so in the given example, there might be 4 `success_api_response/2`.
Inline code is _often_ easier to read and reason about, even when you aren't concerned about a function mutating a parameter (as in the case with elixir).
What this refactoring essentially do is to segment code in natural language blocks. For people who process things in natural language concept blocks this makes it easier to get the general idea of how the function works at a glance and diving in the implementation of the inner functions later.
This will of course introduce the overhead that you have mentioned that will increase the time for you to process the detailed flow of code. But as far as I can see it is only an overhead and by spending some time (or smart IDE or a pencil) there will not be a fundamental problem.
For people that are good at reading raw code without the help of concept-based segmentation, they may not experience the benefit but only the overhead.
http://number-none.com/blow/blog/programming/2014/09/26/carm...
I want to add a third condition that makes this decision easier IMO, because I think it is insufficient to say "if it is a pure function you can extract it". It can often be the case, but it can also lead to what you describe regardless.
It is insufficient if the extracted function is just a one-off helper. The remaining downstream code should read as an _abstracted_ form. Meaning the naming of the functions is clear, uniform and in the best cases they represent a domain layer: A mini-language that communicates clearly what is going on, but draws away from the details. And most importantly it should follow John Ousterhout's proposed rule about interface surface to implementation depth ratio, which should be kept as small as possible.
To the example: I don't think the amount of cases handled should have any implication on extracting functions here (as mentioned in the example). It would still be clearer to just return the data directly. Nothing is gained until the body of the functions themselves have enough _depth_ to provide a useful abstraction, the function names would naturally fall out of what they provide instead of just being a different way to communicate their body contents.
With that said, I don't have a problem with small functions. If named properly can serve as documention for code readers, can hide and limit coupling of implementing details, ect.
> Inline code is _often_ easier to read and reason
Until it's not. And I don't understand when that happens but I know when it does.
It's very easy, as soon as the number of lines increase. I fix the arbitrary limit that a function should fit into a terminal to 72x24. Take your own limit.
Interesting, as I thought more about this, I noticed that I evaluate whether creating a separate function would create a too large of cognitive load in jumping around, and if so, I'd inline, even if it exceeds the 72x24 rule.
It's such an art. It's a sign of a master craftsman, when one knows when to break the rules.
However, in a well designed functional system, this shouldn't be an issue. You're not supposed to jump into other functions, those functions should be labels for values. Reading `success_api_response(body)` should make you think "the API response for a successful request with `body`, not `{:ok, body}`. Checking the type should only reinforce this view. This is the very nature of good abstractions in a language such as Elixir.
In comparison, a badly designed system of abstractions is one in which you must jump to other functions to understand what they do. If `f` is used as anything but a general function parameter, naming a function `update` with no clear reference to what is being updated, other overly generic names such as `calculate` or `execute` with input types that arent `RPNString` or `Process`.
In this specific case, I suspect the x_error_api_response would end up becoming half a dozen different functions and understanding what get_customer returned based on the Env struct would require quite a few trips back and forth.
It's a much smaller nit, but I also found the fix for "complex extraction clauses" slightly less readable than the original: https://github.com/lucasvegi/Elixir-Code-Smells#complex-extr...
1. Statically for known function calls 2. For dynamic dispatch /pattern matching, run a test , record the actually executed code and then inline that.
Sometimes I wish editors had a feature to render an inline view when requested, however that is probably a close-to-impossible feature to add.
It makes me want to stop and do something else, something more pleasant. It evokes a nasty image of shovelling shit, where I want to think of what I am doing as polishing a raw gem.
Furthermore, to me it seems kind of disrectful to numerous code authors of such 'smells'. Maybe they didn't know better, maybe it used to be 'clean' but they were in a hurry to fix something. Maybe they made a different trade off. Labeling what they did as 'smelly' seems impolite and unnecessarily judgemental.
Objectively, it also narrows the perspective: something that smells is harmful, stale or poisonous. It affects disgust and moves us away from it. Food that smells has gone 'bad', which implies it is not good (anymore). So to fix a smell, is to move from bad to good, which is a very binary change. It's either good or bad.
But I think with code this not always the case, and even when some code is clearly better, seeing improvements as more appropriate or efficient tradeoff is almost always much more helpful.
I and my colleague follow a simple rule: Be mean to the code but friendly to the person.
People make mistakes, are in a hurry, don't know any better, sure. But that doesn't make the code any less smelly, buggy, crappy or hard to work with. It's much more productive and even fun to just call it what it is. But this only works in an environment where people are comfortable with each other, take responsibility and want to pragmatically improve and write better code.
What word would you use instead?
We are interested in knowing Elixir's community opinion about these code smells. Collaborations can be made via GitHub issues (https://github.com/lucasvegi/Elixir-Code-Smells/issues) or Pull Requests in our repo!
The refactored example forces the caller to use the specific function for each struct type which can be good or bad depending on the goal of the code.
Also due to syntax colouring I find the "bad code" actually more readable.
But, if you're constantly sending large messages between processes, that might be a sign that those two processes are not as independent as you though, and it might be good to think if there's a way to merge them into a single process.
Another option might be see if you can transform those messages into ref-counter binaries, which will make sending messages quite cheap, since you're sending a single reference to the binary, instead of the whole thing. There's a tradeoff here, of course, depending on how expensive it is to transform your data into a binary, as well as increased complexity of garbage collection of these binaries.
Large messages are just the norm.