10 ms·
I'm sure this is a wonderful intellectual exercise in computer science and math, but if this is to be the advertisement in favor of functional programming, I wo
by fleddr 4y ago
I'm sure this is a wonderful intellectual exercise in computer science and math, but if this is to be the advertisement in favor of functional programming, I wouldn't consider myself a customer.
You start out with code that will not win any beauty awards but it gets the job done. It's easy to understand by programmers of any seniority, it's simple to debug and reasonably easy to put in an automatic unit test.
Next, you add all kinds of vague, poorly named meta utilities that don't seem to solve any tangible real world problem. You even mess with built-in JS methods like map. The code is now harder to understand and more difficult to debug.
A massive pain is added for some theoretical purity that few even understand or will benefit from. I'll double down on my rebellion by stating that the portability of code is overrated.
Here we're writing code to format notification objects. In our overengineering mindset, we break this down into many pieces and for each piece consider reusability outside this context to be of prime importance.
Why though? Why not just solve the problem directly without these extra self-inflicted goals? The idea that this is a best practice is a disease in our industry.
Most code that is written prematurely as reusable, will in fact never be reused outside its original context. And even if it is reused, that doesn't mean that outcome is so great.
Say that the sub logic to format a user profile link turned out to be needed outside this notification context. Our foresight to have made it reusable in the first place was solid. Now two completely different functional contexts are reusing this logic. Next, the marketing department goes: well actually...you need to add a tracking param specifically for links in the notification context, and only there.
Now your "portability" is a problem. There's various ways to deal with it, and I'm sure we'll pick the most complicated one in the name of some best practice.
After 20 years in the industry, you know how I would write the logic? A single method "formatNotification". It wouldn't weirdly copy the entire object over and over again, it would directly manipulate the object, one piece at a time. Error checking is in the method as well. You can read the entire logic top to bottom without jumping into 7 files. You can safely make changes to it and its intuitive to debug. Any junior would get it in about 2 minutes.
Clear, explicit code that requires minimum cognitive parsing.
- oldboyFX 4y agoThank you for writing this. I've come to exactly the same conclusion after a decade of building and delivering complex tangible applications which others have to maintain after I'm gone.
- fleddr 4y agoThanks for the support. I used to live all the best practices, but over time learned that the number one enemy in a codebase is complexity. 90% of software development is maintenance. People doing maintenance are lowly productive because the vast majority of time is spent on trying to understand how things even work, debugging, and making changes in a way that is safe. The reason code is complex are abstractions and dependencies. Many of which are of a questionable or negative value and never deliver their imagined benefits. Hence, instead of "abstract by default" and seeing DRY as some religion, I abstract when value is real, proven and outweighs the massive downsides of doing so. Imagine that we'd need to write a recipe. Sane and pragmatic people would write it as a sequential step of instructions. If a software engineer were to write one, just to follow the recipe you'd need to have 23 additional papers each describing a sub step referred to from the main instructions. Next, we would abstract each substep into "Motion" and "Activity" generalizations. This way, you can reuse "Motion" when you walk your dog later that day. Quite obviously, motions can fail so we extend it with a MotionError, which is a sub class of Error. MotionError has a Logger interface. These 3 super abstractions are useful, instead of just directly saying that you failed a recipe step, we can now apply failure wherever we want, even onto life itself. Since recipes are written in a language, we should abstract Language, itself a composition of sentences, words, characters and glyphs with a Dictionary interface, which itself is an array of Name and Value objects, that we might as well generalize as Thing. Anyway, those were recipe steps. Next, let's do ingredients, also known as immutable multivariant singletons. But not really, because as we follow the instructions, the ingredients morph into something else, which we'll solve with a Morp object, which holds an interface StateChange, consisting of two State objects, which are Things. A ThingsIterator takes care of handling multiple things but not in a way you'd expect, we'd override map() because I really feel my Thing is special. Thinking of recipes, they can potentially come from any source so we'll implement Verbal (an abstraction of Language), Database and the jungle that comes with it, all neatly wrapped into MoreThings. Next we redo all of his by writing unit tests, so that our software that by now implements most of the universe is qualitative. It's math. Or...we can just write the damn recipe.
- zabzonk 4y agoagreed with your first post, much less so than this one, because it is based on analogy. recipes are really not like programming. when i try a new recipe, i normally read the description to see what i am going to get out of it, and if it appeals, give it a bash, without any accuracy. for example, elizabeth david (never a one for accurate measurements) has a great recipe for beef with olives, which comes down to onions, garlick, bay leafs (other herbs to taste) olive oil, lemon peel, beef, black olives, red wine. then just cook them. any experienced cook will have no probs with this - the interesting thing is the combination of beef and olives. all the quantities are somewhat irrelevant and can be adjusted to taste. whereas a computer program must be pin-point accurate in all details analogies == bad. imho sorry i went on a bit there
- draw_down 4y ago
- ozim 4y agoI agree - problem is loads of people think they should write framework and not a simple CRUD app. I remember one meeting where clueless business person told developers (including me) that we need an *abstract* solution for a problem. It took me quite some hours to explain to other devs that for this business person abstract solution is not what they think it is. Business person wanted CRUD app where they could configure names for database entries where developers wanted to make some fancy data types to be used with parametric polymorphism.
- pcblues 4y agoYou nailed it! Language is different in different parts of an organisation or life. A programmer says 'or' to a lay person. A lay person thinks 'xor'.
- wtetzner 4y ago> It wouldn't weirdly copy the entire object over and over again, it would directly manipulate the object, one piece at a time. I think this is the only part I disagree with. Immutable data types make it much easier to understand code, because everything is local. You don't have to worry that some other part of the code base might have a reference to this object, and could be manipulating it out from under you.
- john_the_writer 4y agoIn practice this doesn't really happen in the context of a function call. Like calling do_something(object) might call something inside it that changed the object, but not likely unknown.. Now if you're talking about threading. Honestly, I worked as a c++, java, rails and now elixir dev for > 20 years. I can't tell you how many times I built something that threaded, but not all that often. It's just doesn't come up all that often.
- Tainnor 4y agoIn all likelihood whatever web frameworks you're using in the languages you used will use concurrency in order to be able to serve different requests at the same time. In some languages, that will mean some sort of event loop, but more often than not it will also be done via threads. Just because you create them yourself doesn't mean they aren't there.
- foobiekr 4y agoOne of the things that you imply is another issue: inheriting someone’s functional code is brutal. I’d rather inherit mediocreville php than decipher the “clever” code that someone who got excited by scalaz polished for weeks.
- pcblues 4y agoI think this is key to maintainability. Provably maintainable does not equal maintainable in the real world. Unfortunate? For some. When I wrote software in company X I wrote code simply with old idioms so that anyone could understand, step through, change and fix. Code that can be inspected, stepped through and debugged line-by-line by a graduate is infinitely more useful to a business than an opaque one-line piece of beauty. Unfortunate? For some. But provably correct code is not as important as maintainability in a world of quickly changing requirements. Unfortunate? Not really an issue for the people who pay you.
- jimbokun 4y agoSounds like Go is your ideal language.
- fleddr 4y agoFalse. The best language is logo turtle: http://blog.core-ed.org/files/2014/08/logo_turtle-21.png http://blog.core-ed.org/files/2014/08/logo_turtle-21.png
- sfvisser 4y agoYou're really fighting the wrong fight here. FP is not about prematurely writing reusable code. It's about recognizing patterns in code that exist all over the place and reusing those existing patterns for the new code you're writing. This way you can break your new feature into say 20% newly written domain specific code and 80% reusing existing patterns. Patterns that help with composition, enforcing invariants, simplifying writing test-cases, and more clearly signaling intent. For example, if you provide a monad instance for your "formatting notification objects" I will probably understand how to use your library without even reading a single line of implementation at all. Just by scanning a few type signatures. This way you and your team mates have a huge head start in understanding your new addition to an existing code base. This is a great win for everyone!
- fleddr 4y agoI'm not fighting, only offering an opinion. We simply disagree at a fundamental level. My first and main point is that 80% of software developers, the silent majority of so-called "bread programmers", do not grasp these concepts. So even if the concept is great, it doesn't matter. Second, and this is purely based on experience, I do not subscribe to the idea that your envisioned way of writing a new business feature is very realistic or useful. It leads to dependency hell, difficult to understand code, which is difficult to debug. I don't even subscribe to the idea (anymore) that if you're asked to implement a business feature, that it is implied that you should write it as if it can be torn apart and used for some imagined other feature. Nobody asked for that, we just made that up. One can say that I've become a DRY infidel. I don't expect you to agree, very few would agree. Yet I've arrived here for a reason: 20 years of headaches looking at "best practice" codebases.
- girvo 4y ago> My first and main point is that 80% of software developers, the silent majority of so-called "bread programmers", do not grasp these concepts. So even if the concept is great, it doesn't matter. Right… but is that not the point of these articles? To teach those programmers these concepts so they can get the benefits the GP commenter is talking about? Your comment there seems to presuppose that they can never learn, unless I’m misunderstanding!
- spc476 4y agoYou can apply some FP notions to any code base: 1. Don't use globals. Zero global variables should be the goal, that way, you avoid "spooky action at a distance" where some code here changes a global that changes the behavior of code over there. A function that avoids global variables is easier to deal with than one that doesn't. If you feel you need to use a global, think about why you need one before adding it. Maybe you can avoid it. 2. Parameters to functions are immutable. That way, you won't have to worry about data changing during a function call. If the parameter can be mutated, can you signal the intent in the function name or signature, so the programmer knows that to expect? Generally, try to avoid changing parameters in a function. 3. Separate I/O from processing. Do input, do your processing, then output. God, I wish the major component I worked on did that (concurrent DB requests)---it would make testing the business logic so much easier as it can be tested independently from the I/O. Those three things can be done in any language, and just by doing those three things, you can most of the benefit of FP without the mathematical jargon (Monads? How do they work?). There's no need for over-engineered code, and you can be as explicit as you like while keeping the cognitive aspects to a minimum.
- leidenfrost 4y agoCode readability should be the top most priority when writing code. Even more than portability and performance. Anyone can rewrite a piece of good, succinct, clear code. Even if its performance is poor. That cannot be said for clever hacker code that -was- fast until business requirements changed over time and now the guy that used to understand the code is long gone.
- fleddr 4y agoWell done. Your comment shows actual FP best practices and their immediate, significant advantages. And you did so succinctly.
- john_the_writer 4y agoGods I hate the term monads. I've read the wiki at least 20 times in the past year and it sounds like such bloated rubbish. I love your answer.. And I love how you can use it in every language.