The real problem is branching - when reading code I have to think through two conditional cases.
In this particular example (where you have to validate a client request), I don't see a way out of branching. However I don't think this post has produced the ideal:
JsonParser.parse(request.getBody())
.flatMap(Validator::validate)
.map(ServiceObject::businessLogic)
.flatMap(JsonGenerator::generate)
.match(l -> HttpResponse.internalServerError(l.getMessage()),
r -> HttpResponse.ok(l));
The problem with this is that I have to think through branching all the way through the data flow. However the only function that should branch is validate, to prepare the request to meet the preconditions of the rest of the data flow, all of which should be non-branching.
In other words, I should be able to read this part of the data flow without thinking of branching:
Yes, I've used an if. (If we don't like ifs we can easily get rid of it, of course - but again our problem is branching not the if.)
Why is this objectively better? Because we now have to think about branching wrt to the validation function ONLY. We've minimized where branching matters, and that's solving the core issue.
But don't you see what I'm saying? I assume the order is important, and if the else: (or whatever else you decide to use) were first it would match everything and prevent the other cases from being used. So it's exactly equivalent to an if/elif/else chain. The structure is the same.
Things can be semantically equivalent but more elegant, easier to read, and harder to make mistakes.
I think most function programmers are actually very familiar with the patters found in Java. It's often one of the reasons they fell in love with more functional styles of programming.
Nice because it is readable but I generally dislike clever/elegant solutions for fizzbuzz. Here is my own clever haskell solution:
module Raindrops (convert) where
import Control.Monad (liftM2)
import Data.Foldable(fold)
import Data.Maybe (fromMaybe)
convert = build rules
build = liftM2 fromMaybe show . fold
rules = uncurry rule <$> [(3, "Pling"), (5, "Plang"), (7, "Plong")]
where rule i s j = if j `mod` i == 0 then Just s else Nothing
Short explanation: the fold function combines lists of monoids. In this case it combines three seperate monoids and does `[Int -> Maybe String] -> Int -> Maybe String`. It takes a list of functions that might create a string from a number, gives them all the same input and concats all strings that were returned. If none were returned the result stays Nothing and is then replaced by the string representation of the input via fromMaybe.
I like this solution because it shows how powerful it is to abstract over these concepts but also that trying to be too clever quickly ends in impossible to follow complexities.
The point is not the branching (although it makes a good click-bait title), the point is that we can encode the branching possibilities in our type system, rather than in the permutation of values of variables we have laying around.
While it's "neat" to encode a lot of stuff into types, it also makes code comparatively very difficult to understand for almost everyone. A big part of this is that documenting type systems is even harder and the tooling is even worse than documenting regular code, at least in mainstream languages (like C++; some called me a template guru years back), and of course that many developers are simply not very experienced with complex type systems. For that reason most developers stay well within the bounds of their languages' type system; it arguably gives you code that may be more "plain" and less "smart", but also code that far more developers will be able to reason about efficiently.
Feature rich type systems are really great if you have a good understanding of your types. Often, you don't. One of the best parts of software is it's malleability. Pushing business logic up into types makes it less malleable.
Types don't force you to have a concrete understanding of the problem (although they can help think about it). They help you make concrete decisions about your solution. This can save time when changing things often.
Three times you introduce possible failure, the original parse as well as each call to flatMap. It flattens each of these possible failures into one, though, so you don't have to think about it!
So if the programmer is sane and doesn't hide side effects in there you only have to think about branching at the pattern match - if any step failed do this, otherwise do this!
The real problem is branching - when reading code I have to think through two conditional cases.
In this particular example (where you have to validate a client request), I don't see a way out of branching. However I don't think this post has produced the ideal:
The problem with this is that I have to think through branching all the way through the data flow. However the only function that should branch is validate, to prepare the request to meet the preconditions of the rest of the data flow, all of which should be non-branching.In other words, I should be able to read this part of the data flow without thinking of branching:
So this I believe is objectively better: Yes, I've used an if. (If we don't like ifs we can easily get rid of it, of course - but again our problem is branching not the if.)Why is this objectively better? Because we now have to think about branching wrt to the validation function ONLY. We've minimized where branching matters, and that's solving the core issue.