> the problem is one of call/return in general, whether expressed in an FP language or not. It really wants to
return something
This is certainly a problem with first-order programming, where we might have function signatures like:
foo : A -> B
bar : B -> C
baz : C -> D
And we plumb them together into:
quux : A -> D
quux x = baz (bar (foo x))
IIUC the problem you're describing is that a change causes one of these functions, say `bar`, to gain an error condition. We can represent that by:
bar' : B -> Maybe C
Our `quux` definition would then become:
quux' : A -> Maybe D
quux' x = case bar' (foo x) of
Nothing -> Nothing
Just y -> Just (baz y)
I don't think it's too bad to have a failing function return a special value like `Nothing` to indicate that: `return Nothing` is no more difficult than `throw SomeException`, `exit 1`, etc. The problem is having everything in-between the error and its handler having to propagate it, like above. This sort of 'check if successful, return Nothing if not' (anti-)pattern seems to crop up
a lot with e.g. Java programmers being introduced to `Optional`. It seems like we're going to drown in null checks (or Nothing checks, in this case).
Yet this problem isn't so bad if we use higher-order programming (functions of functions, etc.). In particular, we can write the first `quux` as a higher-order combination of those other functions (where `f ∘ g` is composition of `f` and `g`, i.e. a function which applies `g` to its argument, then applies `f` to that):
(f ∘ g) x = f (g x)
quux : A -> D
quux = baz ∘ bar ∘ foo
This ability to compose is really powerful. The FP community (taking their terminology from maths) call these composable things "categories", and functions are one example of a category; another is circuitry, where we can wire the output pins of one circuit to the input pins of another. In the context of this article, a (non-branching) railroad track is an example of a category (we can place one piece of track after another). There's a nice description of categories at
http://www.haskellforall.com/2012/08/the-category-design-pat...Now what if we then need to propagate error conditions? We can just treat that error-propagating ('check if successful, and return Nothing if not') functionality as an alternative form of composition, which I'll write as `⋅`:
(f ⋅ g) x = case g x of
Nothing -> Nothing
Just y -> Just (f y)
quux' : A -> Maybe D
quux' = baz ⋅ bar ∘ foo
In fact we can be a little more general by defining a `map` function instead; this lets us do error-handling even if we only have a single function, and we can re-use the normal ∘ composition:
map f x = case x of
Nothing -> Nothing
Just y -> f y
quux' : A -> Maybe D
quux' = map baz ∘ bar ∘ foo
This looks like the same amount of boilerplate as the first-order version, but operations like `map` are general enough that they'll probably be included in the language by default, or at least with the library that defines `Maybe`, `Just` and `Nothing`. Hence the only code we need to write to go from `quux` to `quux'` is `map`.
Just like with composition, lots of things can be 'mapped'. FP calls such things "functors", although beware of other uses for that term (e.g. some languages use this phrase to mean function objects; some, like Ocaml, use it to refer to module transformers). Another example of a functor is lists, where `map` applies a function to all of a list's elements. In fact, if we think of lists as "containing any number of values" then we can think of `Maybe` as "containing at most one value". Going the other way, if we think of `Maybe` as being "a result which either failed or succeeded" then we can think of lists as being "a result which either failed or succeeded in any number of ways", i.e. lists are just non-deterministic computations (e.g. see Wadler's "How to Replace Failure by a List of Successes" https://rkrishnan.org/files/wadler-1985.pdf )
Another example of a functor is functions: a function `g : A -> B` can be thought of as 'containing' some `B` return values, which we can 'access' by providing an `A` value as input. If we have a function `f : B -> C` we can apply it to those return values by doing `map f g : A -> C`. We get a new function 'containing' `C` return values, which we can 'access' by providing an `A` value. How does such a function work? It just takes the `A` value that we give it, passes it into `g` to get a `B` return value, then passes that into `f` to get the final `C` return value. In other words, the `map` function for functions is just the `∘` composition operation!
Another example of a functor is an I/O action, for example reading a temperature sensor. I/O actions are like functions, in that they 'contain' return values (e.g. temperature readings), which can only be 'accessed' by executing the action (e.g. performing a measurement). Just like functions, we can `map` over these actions as a form of composition: if we have `a : IO Temperature` and `f : Temperature -> Either Hot Cold`, then we can do a form of composition using `map`, where `map f a : IO (Either Hot Cold)` is an action which can measure whether it's hot or cold (by executing `a` then feeding the result into `f`).
Another thing we might want to do is 'unwrap' nested values. For example if we have a possibly-failing function `g : A -> Maybe B` and a possibly-failing function `f : B -> Maybe C` then we can use `map f g : Maybe (Maybe C)`, which might be `Nothing` (if `g` failed), or it might be `Just Nothing` (if `g` succeeded but `f` failed) or it might be `Just (Just something)` (if both succeeded). If we don't care about which one failed, we can use a function called `join : Maybe (Maybe A) -> Maybe A` to replace `Just Nothing` with `Nothing` and `Just (Just something)` with `Just something`.
FP people call joinable things "monads", and in the case of `Maybe` we can think of it as concatenating together a list-of-at-most-one-(lists-of-at-most-one-element) to get a list-of-at-most-one-element; or as turning a combination of error cases into a single error case. For lists, we can think of `join` as concatenating lists-of-lists; or as turning chains-of-successful-results into successful-results. For functions we can think of `join` as making a function which re-uses its argument for the subsequent step. For I/O we can think of `join` as turning an action-which-measures-which-action-to-take-next into an action-which-measures-then-does-something.