8 ms·
The first example is bad for reasons not related to ifs and fors. In general, if you can, if you have a "container" for something, you should write functions o
by bcrosby95 3y ago
The first example is bad for reasons not related to ifs and fors. In general, if you can, if you have a "container" for something, you should write functions on the contained, domain-level "Thing" rather than the container with the domain level thing.
As an example I work with - Clojure. Sometimes I use agents. I don't write functions for agents, I write functions for things agents might contain.
Similar rules for Elixir. My primary domain level functions don't work off a PID. They work off the underlying, domain-level data structures. GenServer calls delegate to that where necessary.
This makes them more flexible, and tends to keep a cleaner distinction between a core domain (frobnicate the Walrus) and broader application concerns (maybe the Walrus is there, maybe not... oh yeah, also frobnicate it).
- crdrost 3y agoYeah. I think the given advice probably takes validation logic and floats it too high. It is of course nice to have early validation logic, but it is also nice when your functions don't mysteriously crap out with some weird error but instead shout a validation error at you. Haskell solves this with newtypes, “here is a transparent container that certifies that you did the appropriate validation already,” that helps for this. The advice that I really want to hammer into people's heads is, prefer “sad ifs.” That is, I will almost always find this if (something_is_wrong_in_way_1) { // fix it or abort } if (something_is_wrong_in_way_2) { // fix it or abort } if (something_is_wrong_in_way_3) { // fix it or abort } more readable and maintainable than this if (things_are_ok_in_way_1) { if (things_are_ok_in_way_2) { if (things_are_ok_in_way_3) { // do the happy path! } else { // fix or abort thing 3 // if fixed, do the happy path } } else { // fix or abort thing 2 // if fixed, test way 3 again // if way 3 is good do the happy path, else fix it // ... } } else { // ... } I feel like it's in human nature to focus on the expected case, I want everyone whose code I meet to do the exact opposite, focus primarily on the unexpected. Every “if” imposes a mental burden that I am keeping track of, and if you have to go to an external system to fetch that information, or you need to exit early with an error, I can immediately discharge that mental burden the moment I know about it, if the handling and the detection are right next to each other.
- quickthrower2 3y agoIn short: avoid `else`. Both what you mention, and also the case with : if (...) { ... } else { return 0; } Which can become if (...) { ... } return 0;
- hedora 3y agoEven better, if you're using a language with pattern matching (rust, any ML, haskell), just put it in a single match with the happy case on top, and the error cases below. To do this, you need to encode the distinction between happy and sad path in your types. That's good for all sorts of other reasons, so it's not really a drawback.
- KineticLensman 3y agoYes, pattern matching really helps with clarity, especially when there are multiple happy paths, like in a parser
- theteapot 3y agoWhat about `else if`? Not a fan?
- quickthrower2 3y agoOccasional vice for me.
- stouset 3y agoPersonally I only either use if statements that wrap a single line of logic, or a switch/case statement where each branch is a single line of logic. Most situations are handled by guard clauses, exhaustive type switching, or switching on logical cases. Very occasionally do I have to break this practice for a legitimate reason, but this keeps functions simple to read and to reason about. I occasionally am unable to avoid an else, but I will always avoid else if. It puts too much reliance on remembering how this condition interacts with the one on the if statement above it, which can often be many lines away. I’d infinitely rather use a case statement where all the alternatives are in an easily-understood table with every condition aligned on successive lines.
- GuestHNUser 3y agoCorrect me if I am misunderstanding, you are saying the first example would be better if it was `walrus.frobnicate()`? Isn't that a syntax preference more than an issue with the point the author is trying to make?
- deleted 3y ago[deleted]
- runeks 3y ago> As an example I work with - Clojure. Sometimes I use agents. I don't write functions for agents, I write functions for things agents might contain. I'm not following you. Do you have a better example?
- bcrosby95 3y agoSimilar to the example of the OP: (def walrus (agent {})) ; good (defn frobnicate [w] (assoc w :frobnicated true)) (send walrus frobnicate) ; bad (defn frobnicate [w] (send walrus #(assoc % :frobnicated true))) (frobnicate walrus) It might make sense to also have a function that executes something like (send walrus frobnicate). But IMHO code should basically never look like the bad example.