1

To the best of my understanding Monads were created to allow for composing functions with those that had potential side-effects - loosely speaking.

For me composition implies code like so:

f(g(h(x)))

In order to achieve this in a programming language one has to "line up the types" correctly, so that the output of h(x) is an input to g(...). Implying that such a function chaining would require all functions in the chain to work at the "monadic level of abstraction" for the types to line up correctly.

However, at my workplace (and some library code) I see a lot of code that looks more like "function chaining" like so:

h(x).flatMap(g).map(f)

This is NOT function composition AFAIK and this probably makes code harder to read IMHO since there's cognitive overload in understanding "type translation" with flatMap/map thrown into the mix. One has to mentally unravel the computations to see how they all "line up".

What is the common convention in the FP-world? I had a few discussions with my peers and got extremely strong push back for the compositional style f(g(h(... - almost everyone preferred the "chaining style". Is there a common "style guide" that's advocated for something like this?

From my POV, I've been exposed to LISP/Scheme and f(g(h... isn't really alien and is rather more clean and reads like a DSL. The chaining is rather hacky.

Question: Should functions work at the monadic level to allow for composition or is the suggestion to work at the level of the wrapped value?

Concrete example:

checkForBlanks(csvRows).flatMap(checkForUniqueIds).map(buildCache)) 

vs

buildCache(checkForUniqueIds(checkForBlanks(csvRows)))

Method signatures (Non-monadic):

def checkForBlanks(csvRecords: Vector[Record]): Either[InternalDomainError, Vector[Record]]

def checkForUniqueIds(csvRecords: Vector[Record]): Either[InternalDomainError, Vector[Record]]

def buildCache(csvRows: Vector[Record]): MyCache 

Method Signatures (Monadic):

def checkForBlanks(csvRecords: Vector[Record]): Either[InternalDomainError, Vector[Record]]

def checkForUniqueIds(data: Either[InternalDomainError, Vector[Record]]): Either[InternalDomainError, Vector[Record]]

def buildCache(data: Either[InternalDomainError, Vector[Record]]): MyCache 

Common points for pushback:

  • Composing forces reading right to left
  • Composing makes functions think of Monads and will clutter responsibility of handling wrappers
  • It's easier if a function just works on the "actual value" vs. a monadic wrapper since it's "easier to reason"
  • It's way more flexible to "chain" than compose
  • If you really want to "compose" add additional methods that "call out" to pure methods and interally wrap monads - unnecessarily complicated so don't do it: E.g.:
def uniq(data: Either[InternalDomainError, Vector[Record]]): Either[InternalDomainError, Vector[Record]] =            data.flatMap(checkForUniqueIds)

From an FP-adherence and best practices POV what's the recommendation on should one do this for readability/maintainability?

  • There isn't any right or wrong answer here. Each method has its specific use cases and pros/cons, some of which you've already stated in your question. – Robert Harvey Apr 26 at 21:57
  • @RobertHarvey - I understand there isn't really a right/wrong - I just want to know what is commonly practiced/touted...is one approach more favored than the other? – PhD Apr 26 at 21:59
1

The method used is the one most appropriate for the task at hand.

To put some of this into perspective, I recognize both map and flatmap as well-known, well-understood functional programming mechanisms for operating on collections, even though I'm not a functional programming expert. Chaining makes sense in this context. In C#, these concepts are used in Linq, where method chaining is used in a very powerful way to compose set operations. Data types don't matter, because everything in the call chain is the same type IEnumerable<T>. There are even state-engine-like ways to defer execution.

But if you're just working with ordinary functional composition (and not composing sets), f(g(h(x))) probably makes more sense. While Linq uses method chaining rather effectively, there are many other scenarios where it's ... shall we say, gratuitous? Fluent interfaces in C# and Java are considered (for the most part) boutique creations, because it's easier, cleaner and generally better overall to simply call a well-designed function or constructor.

  • The answer "it depends" can be considered a blanket answer for almost all concerns in software engineering. My ask is very specific to functional programming's "suggested" practices of composition. I understand that you "can" chain. But just because one "can" do it doesn't imply it's "best practice". Yes, limitations may dictate suboptimal practices but there aren't any technical limitations per se. I want to look at this from an abstraction POV - which approach has more merit and if I'm missing any other pro/con? Or as a functional programmer what's the most common route encountered? – PhD Apr 27 at 5:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.