I don't find FP those concepts any harder to grok than GoF design patterns (in fact, I've never actually gone through the latter in depth; I just see them mentioned occasionally and have learned some via osmosis).

In fact, those particular concepts you list at the end (monad, monoid, functor) are particularly easy: they're just different ways to think about lists (which we can then generalise to other things). I think the main obstacle is terminology, which makes things feel unfamilar.

For example, two lists can be appended together: programmers do this all the time, so it's a very familar operation; hence it might be useful if we could take this intuition and apply it to other situations. One interesting fact about list append, which we often take for granted, is that nesting doesn't matter, e.g. these will always be the same (modulo runtime: the first traverses 'bar' twice, the second traverses 'foo' twice):

foo.append(bar.append(baz))
foo.append(bar).append(baz)

We can capture this idea of "combining where nesting doesn't matter" using an interface, e.g. an OOP programmer might write an 'Appendable' interface with an 'append' method. We can implement 'Appendable' for lists, but we can also implement it for other collections like arrays, sets, or even implementation-specific collections like some 'big data' library that distributes values over a cluster of machines.

An interesting thing happens if we try to implement 'Appendable' for key/value mappings: we have to deal with key collisions (the same key appearing in both mappings). One way to handle this is to always keep values from the first mapping; another way is to always keep values from the second mapping. There's also a third possibility: if the value type is also 'Appendable', we can 'append' the conflicting values together! So far, this seems like a neat little API to expose in a package or standard library.

However, there's nothing about this interface which is specific to collections! We can look through our standard programming toolkit for other things which happen to be 'Appendable' too (AKA "combined where nesting doesn't matter"). One obvious example is numbers: addition is a valid way to 'append' numbers, as are multiplication, 'max' and 'min'. This works well with the key/value example above, e.g. we might have a bunch of mappings which count occurrences of something; we can append those mappings together by adding conflicting counters, to get an overall count of all occurrences; if we instead map keys to the largest observed value of something, we can append those mappings by maxing conflicting values, to get the overall largest values; and so on, as a way to divide and conquer our problems.

Note that averaging is not a valid 'append' for numbers since nesting matters, e.g.

mean(mean(1, 3), 4)
= mean(2 , 4)
= 3

mean(1, mean(3, 4))
= mean(1, 3.5)
= 2.25

However, if we keep a pair of total/count, they can be 'appended' by adding separately (and not reducing), e.g.

mean(mean(1/1, 3/1), 4/1)
= mean(4/2 , 4/1)
= 8/3

mean(1/1, mean(3/1, 4/1))
= mean(1/1, 7/2)
= 8/3

The name 'append' doesn't quite capture what's going on here, but it makes sense if we squint a little.

Another form of 'Appendable' value is "optional" or "erroneous" results (e.g. Maybe<T>, Option<T>, Try<T>, Either<Error, T>, ParseResult<T>, etc.). To 'append' two such values together we check the first one to see if it succeeded: if so we return it, otherwise we return the second. (Note that this faces the same ambiguity as for key/value mappings: we could begin by checking the second value instead; or, if the results are themselves 'Appendable', we could 'append' them together iff both were successful!).

In this case it looks like the name 'append' is misleading, since we're actually performing error handling/recovery! This might seem weird: in both OOP and FP we're meant to program to interfaces rather than implementations, but in this case generic code like 'foo.append(bar)' might mean "append the contents of bar to the end of foo's contents", or "add the numbers foo and bar", or "if foo failed try recovering with bar instead". Those seem to be very different things, so what's going on? Well, we can think about it from two different perspectives:

- An "optional" or "erroneous" type is a form of collection: it either contains a single value (a successful result), or no values (an error occurred). They're like lists with at most one element (perhaps with some error information in the empty case). The error-handling behaviour of 'try the first value then the second' is exactly the same as 'append two lists-with-at-most-one-element' (if the first list has an element we use that, otherwise we look in the second list).

- Looking at it the other way around, if a "list with at most one element" represents a potentially optional/erroneous result, then a "list" represents a value with any number of successful results (including zero)! From this perspective, when we append two lists together we create a value with all of the results of both; this is a form of error handling (once we've exhausted the results in the first list, we move on to the second)!

There's a nice paper called 'How to Replace Failure by a List of Successes' which explores this in more detail ( https://rkrishnan.org/files/wadler-1985.pdf ). In particular this 'error handling with multiple results' is actually backtracking search (as found in logic programming)! If we append the lists we get a depth-first search; interleaving gives us a breath-first search.

We seem to be straining to idea of "appending" a little, but it gets worse! Another standard programming construct which turns out to be 'Appendable' (AKA "combined where nesting doesn't matter") is functions (AKA procedures, methods, programs, etc.). In this case we use function composition, e.g. appending two functions like 'append(f, g)' gives us a new function like 'x => f(g(x))'. Functions (especially pure functions) are a bit like key/value mappings: calling a function on some argument is like looking up some key in a mapping; hene we can think of functions as "collections" of their return values. However, the implementation of 'append' for functions is very different to what we did for key/value mappings: it fits the interface, and is a common and useful operation, but is it really a good idea to think of this as 'appending'? We're appending the processing steps, but that requires thinking at a higher level of abstraction.

Since composing functions doesn't run them, we can actually compose programs written in a different language to the one we're using! As a very simple example, here's some Javascript which composes Python lambda functions (represented as strings):

const append = function append(f, g) {
return "lambda x: (" + f + ")((" + g + ")(x))";
}

console.log(append("lambda x: x+1", "lambda x: 2*x"));

> "lambda x: (lambda x: x+1)((lambda x: 2*x)(x))"

The first lambda is an increment function, the second is a doubling function; 'appending' these gives a lambda which doubles its argument then increments it! This sort of thing is useful in metaprogramming, compiler implementations, etc., although it's generally done with ASTs rather than strings.

Hopefully it's clear that this very simple 'Appendable' interface crops up in all sorts of situations: I named it after the behaviour for lists, but it turns out to describe many more things where my terminology doesn't quite make sense.

Functional programmers use this interface a lot, although they call it 'Semigroup' (a term inherited from algebra). I think that's a pretty awful name, but my "OOP-friendly" alternative also turned out to be pretty bad too (naming things is hard, after all!); hence we shouldn't be too worried about the names. It's much more helpful to grasp some origin for the idea (e.g. appending lists), then gradually work our way out into less familiar examples.

You asked about "monoids": that's the same as Semigroup (AKA 'Appendable'), except it also has an "empty" value which leaves things unchanged if we 'append' it. For example; appending an empty list/map/set/etc., adding zero, multiplying by one, averaging with 0/0, 'max'ing with -infinity, 'min'ing with +infinity, composing with the identity function 'x => x' (or "lambda x: x")

If we go down a similar rabbit hole as above, but for 'map(myFunction, myList)' rather than 'append(list1, list2)', we get a 'Mappable' interface; which FP programmers call 'Functor'. It turns out that functions are also 'Mappable', and their 'map' function is also function composition; I like to think of this from the "functions are collections of their return values" perspective.

If we do the same for list concatenation (i.e. combining a list-of-lists into a single list) we end up with a 'Collapsable' interface, which FP programmers call 'Monad'.