Hacker News new | ask | show | jobs
by mgdev 651 days ago
I'm personally a fan of FP. It offers clear benefits: simplified parallelization, improved testability, and reduced side effects. These advantages often lead to more maintainable and robust code.

However, FP's benefits can be overstated, especially for complex real-world systems. These systems frequently have non-unidirectional dependencies that create challenges in FP. For example, when component A depends on B, but B also depends on a previous state of A, their interrelationship must be hoisted to the edges of the program. This approach reduces races and nondeterministic behavior, but it can make local code harder to understand. As a result, FP's emphasis on granular transformations can increase cognitive load, particularly when dealing with these intricate dependencies.

Effective codebases often blend functional and imperative styles. Imperative code can model circular dependencies and stateful interactions more intuitively. Thus, selectively applying FP techniques within an imperative framework may be more practical than wholesale FP adoption, especially for systems with complex interdependencies.

4 comments

>d. As a result, FP's emphasis on granular transformations can increase cognitive load, particularly when dealing with these intricate dependencies.

Does it increase cognitive load, or is it just making the cognitive load more apparent. Sure it's easier to write multithreaded code if you assume race conditions can't happen, but that's not actually accurate to what would happen.

Perhaps FP just makes explicit in the typing/coding portion, what would otherwise be uncovered hours/days/weeks later in a bug?

Also worth mentioning in terms of mixing functional/imperative techniques: it can be very helpful to use languages (and frameworks, libraries, interfaces) which are functional-first/-by default, not necessarily pure but which provide specific affordances for managing side effects. This can be seen in languages like Clojure (with reference types distinct from most of the rest of the language/stdlib). It’s also a hallmark of many projects with a reactive paradigm (which in some form or another have dedicated APIs for isolating state and/or effects).

These aren’t strictly necessary for effectively using functional techniques in an imperative environment, but they can go a long way toward providing useful guardrails you don’t have to figure out yourself.

I don't know exactly what you are trying model with this hypothetical circular dependency.

However, circular dependencies can be represented with lazy (aka non-strict) references and deferred function calls (aka thunks / call-by-name), and are IMO easier to reason about than mutable imperative techniques for representing such relationships. They also have the advantage of being totally thread-safe.

The OP (Lihaoyi) is the author of an important set of Scala libraries, and Scala is an example of what you are describing. Scala is a hybrid OO / FP language that is not dogmatic about purity. You can be totally imperative if you want.

It is common in the Scala ecosystem to implement performance-critical library code using local mutability and null references. Internally the function is imperative; to the caller, it is functionally pure.

It offers none of those things and provably doesn’t have more robust code.

Maintainable code? I dunno. From what I’ve seen, FP is much worse for changing when you have shit deep in your call stack. I wouldn’t call that maintainable.

More readable? Nah. Nearly everyone has a much easier time consume code when they’re not constantly context switching between function jumps all over the place.

Additionally, FP only makes threading “easier” in one very specific circumstance. Once you need to parallelize the same workload, FP is generally much much harder to thread, whereas this is trivial in other languages.