This may be an unpopular opinion, but I like some of the ideas behind functional programming.
An excellent example would be where you have a stream of data that you need to process. With streams, filters, maps, and (to a lesser extent) reduction functions, you’re encouraged to write maintainable code. As long as everything isn’t horribly coupled and lambdas are replaced with named functions, you end up with a nicely readable pipeline that describes what happens at each stage. Having a bunch of smaller functions is great for unit testing, too!
But in Java… yeah, no. Java, the JVM and Java bytecode is not optimized for that style of programming.
As far as the language itself goes, the lack of suffix functions hurts readability. If we have code to do some specific, common operation over streams, we’re stuck with nesting. For instance,
var result = sortAndSumEveryNthValue(2,
data.stream()
.map(parseData)
.filter(ParsedData::isValid)
.map(ParsedData::getValue)
)
.map(value -> value / 2)
...
That would be much easier to read at a glance if we had a pipeline operator or something like Kotlin extension functions.
var result = data.stream()
.map(parseData)
.filter(ParsedData::isValid)
.map(ParsedData::getValue)
.sortAndSumEveryNthValue(2) // suffix form
.map(value -> value / 2)
...
Even JavaScript added a pipeline operator to solve this kind of nesting problem.
And then we have the issues caused by the implementation of the language. Everything except primitives are an object, and only objects can be passed into generic functions.
Lambda functions? Short-lived instances of anonymous classes that implement some interface.
Generics over a primitive type (e.g. HashMap<Integer, String>
)? Short-lived boxed primitives that automatically desugar to the primitive type.
If I wanted my functional code to be as fast as writing everything in an imperative style, I would have to trust that the JIT performs appropriate optimizations. Unfortunately, I don’t. There’s a lot that needs to be optimized:
- Inlining lambdas and small functions.
- Recognizing boxed primitives and replacing them with raw primitives.
- Escape analysis and avoiding heap memory allocations for temporary objects.
- Avoiding unnecessary copying by constructing object fields in-place.
- Converting the stream to a loop.
I’m sure some of those are implemented, but as far as benchmarks have shown, Streams are still slower in Java 17. That’s not to say that Java’s functional programming APIs should be avoided at all costs—that’s premature optimization. But in hot loops or places where performance is critical, they are not the optimal choice.
Outside of Java but still within the JVM ecosystem, Kotlin actually has the capability to inline functions passed to higher-order functions at compile time.
/rant
Circular dependencies can be removed in almost every case by splitting out a large module into smaller ones and adding an interface or two.
In your bot example, you have a circular dependency where (for example) the bot needs to read messages, then run a command from a module, which then needs to send messages back.
v-----------\ bot command_foo \-----------^
This can be solved by making a command conform to an interface, and shifting the responsibility of registering commands to the code that creates the bot instance.
main <--- ^ \ | \ bot ---> command_foo
The
bot
module would expose theBot
class and aCommand
instance. Thecommand_foo
module would importBot
and export a class implementingCommand
.The
main
function would importBot
andCommandFoo
, and create an instance of the bot withCommandFoo
registered:It’s a few more lines of code, but it has no circular dependencies, reduced coupling, and more flexibility. It’s easier to write unit tests for, and users are free to extend it with whatever commands they want, without needing to modify the
bot
module to add them.