I'm not sure where to start, since "I literally understood nothing" is quite vague, but totally understandable. I agree with Campbell's assessment on the tutorial: it does seem to go much into the motivation (why
you should care about any of the points listed), instead assuming that you already kind of know what Functional Programming is all about. I'll try and walk through the first of those three articles and rephrase to try and shed some more light on the subject. If you have more specific questions though, that would be really helpful for us to understand what you're feeling unsure about and better target our answers.
I'm going to stay away from the examples given in the article. They seem a little weird to me, and at least the first one is not quite the right way to use the Stream API. Instead, I'll try and stick to concepts and only show (sort of) the Java APIs once the concepts have been introduced.
The three main points listed by the article really fall under two important ideas: functions are first class, and so can be passed around, even to other functions; and functions should be "pure", depending only on their inputs to generate their outputs. Together, these two ideas form a programming paradigm where the function is king and mutation is limited. This is where all the hype comes from: modern development likely needs to take advantage of multiple cores, which means a certain degree of parallel processing. Functions that don't mutate and have predictable outputs based on their inputs are much easier to reason about (and debug!) than objects and methods that have mutable internal state.
I'll try to showcase how functional programming can help make it easier to reason about a problem with a (hopefully not too) simple example: we have a list of people's ages and we want to calculate the youngest adult age (>= 18).
Starting with a more imperative approach, we might try something like:
There are a few interesting things going on here. First, youngestAdultAge
is (potentially) assigned to multiple times. Here we don't really have a problem, aside from the potential for mistakenly reassigning the variable, but imagine if we had a huge number of ages to process: it wouldn't be obvious how we might go about parallelizing this implementation without the multiple threads clobbering each other by writing to the same variable. Second, the logic is a little spread out (capturing the youngest age is spread over the declaration, half of the if
and the assignment). And third, we'd have to copy/paste the whole function with a few changes if we wanted to find, for example, the oldest child.
So let's consider what we might need here if we want to instead approach the problem in a more functional way. First, let's write some functions: one to determine if a given age was >= 18, and another to determine the minimum of a two ages:
The first function is what we would call a Predicate
: it's a function that, given some input, makes a decision about that input and returns either true
. There really isn't any difference between a "predicate" and a "function"; a predicate is just a function that returns a Boolean. Typically, predicates are used to select elements out of a sequence (i.e. decide which elements should be processed and which should be ignored).
This is where the notion of "higher-order" functions (functions that take functions) comes in: imagine that our sequence of ages has some method on it, let's call it filter
, that takes a Predicate and applies it to each element of the sequence. Selecting all the adult ages would then look something like (I'm assuming you're familiar with lambda syntax):
So now we have all the adult ages, we want to find the youngest (minimum). We have our function for that, but how would we use it? What we'd like to do is take that filtered sequence of ages and reduce it to a single value: the youngest age. Image that our sequence of ages has another method on it, let's call it reduce
, that takes a function to apply over and over to the sequence of ages until the result is a single value. Finding the youngest adult would then look something like:
The reduction may require a little more explanation than the filter. I've called the two arguments to the function youngest
because the first is there to track the youngest age in the same way as the youngestAdultAge
in the imperative solution, and the second is the age we are currently comparing against the youngest. To visualize what's going on, consider this sequence of ages: [32, 22, 40]. The reduction does the following:
For completeness, we'll use a two-argument variant of reduce
that takes an identity
as its first argument. This is an identity in the mathematical sense: including it in the reduction should not cause any change to the calculation. For addition, the identity is 0; for multiplication, the identity is 1; for our current example, we've already seen the identity in the imperative implementation: Integer.MAX_VALUE
. Here are the two implementations together for comparison:
You can see here that the functional implementation is much closer to the problem statement than the imperative one. Also, because it takes advantage of higher-order functions, it's trivial to change what we're looking for. Going with the earlier example of oldest child, we would just have to change what we plug in:
I hope that helps!