There are some things that should be addressed here:
Properly functional code is really without side effects; that means not just minimizing inline IO but eliminating it by returning IO actions as data side effects and applying the results through pure functions. Usually, you’ll put all the IO in an interpreter loop that lives outside the computing code; the result is that the code that does anything fancy is 100% testable and the cradle that runs can be swapped for a tester at any time.
Although you can do the same thing with OO, the vocabulary of OO languages makes yielding side effects less natural and exhibits time-coupling unless you write your objects in functional style (which is getting easier to do; use “data class” and “val” for most things in Kotlin, and you’re basically there).
When writing C# or C++, one should definitely pass in interfaces that provide system access rather than couple to them directly. Not only does it allow us to set up tricky scenarios we may need to debug but can’t reproduce organically, it also encourages far better design choices. Having too many injected interfaces isn’t a problem, but it’s often a symptom of a problem that’s easy to identify when you can see it. This admittedly mostly applies to large or very complicated system.
“Good programmers will produce good code anyway. Bad programmers will produce unreadable code anyway.” Is a false dichotomy. Not all readable code is good in context. Not all difficult code is necessarily bad. Neither of these things is a full answer. Functional code definitely doesn’t make code more readable, but here’s one thing it does do; when done properly, anyone can look at the code and say: I can analyze this and only this and know everything. I can replace this and only this and fix everything. That’s the value proposition of functional programming in a nutshell. It’s up to you to decide where you draw lines and tradeoffs that give away a bit of this for expedience.
That having been said, permissive languages like OCaml, F# and elixir rest a lot of power in your hands. If you freely mix imperative IO with code that does computation, then you’re trading away a lot of what the language is making easy for you. I’d suggest giving it a try with clear boundaries between functional and imperative code before declaring the whole thing a fraud.
If you don’t like it that’s also no big deal. For my part, it’s been a reaction to some unhealthy ways I developed as a programmer, a few bad habits I’ve picked up, and a desire to think more analytically. I also haven’t set aside imperative languages entirely, but I’ve course corrected and used the habits engendered by writing purely functional code to improve my OO code as well. Ultimately, what you get out of functional programming depends on you.
let you’ = fp(you)