Hughes is absolutely right, and the following paragraph from his paper hits the nail right on the head:
Such a catalogue of “advantages” is all very well, but one must not be surprised if outsiders don’t take it too seriously. It says a lot about what functional programming isn’t (it has no assignment, no side effects, no flow of control) but not much about what it is. The functional programmer sounds rather like a medieval monk, denying himself the pleasures of life in the hope that it will make him virtuous. To those more interested in material benefits, these “advantages” are totally unconvincing.
However, the things he mentions that FP does have (higher level of abstraction through gluing functions, lazy evaluation and thus easier modularity) are hard to grasp for mid-Bachelor students. In the end of the course they should be able to understand those advantages when you talk them through it, but the problem is motivating them from the start.
None of these things are particularly difficult to learn, unless you teach them in an inherently difficult style (such as Haskell or "a lesser known more academical functional programming language.") Many functional concepts are useful in the right places, as a "the right tool for the job" sort of thing, but functional languages have a tendency to go overboard and apply these concepts dogmatically rather than pragmatically.
Imperative languages get this right much more often. For example, you want your students to understand lazy evaluation? Teach them to use Python generators or C# iterators. That's what the
yield keyword is: lazy evaluation. But it's set up in such a way that you decide when to use it as appropriate, rather than the language dropping it on you as the default. Want them to understand coroutines? Teach them
async/await. Likewise, there's nothing at all in Hughes's explanations of "program gluing" that can't be easily rewritten in modern-day object-oriented languages.
Today, with LINQ (.NET), the Streams API (Java), and Itertools (Python), basic functional concepts are used all the time in industry. But they're used as "another tool in the toolbox," to be applied as appropriate. When your students balk at functional languages, a large part of the problem is because they don't have this approach; they try to cram all these concepts down your throat whether you want them (or even need them at all!) or not.
A classic example is recursion. Let's say you wanted to find the length of a linked list. In Python, you'd do it like this:
result = 0
while list is not None:
result = result + 1
list = list.next
In Paul Graham's paper "On Lisp", he does it a different way. I'm not going to inflict Lisp upon the audience, (the originals can be found on pages 22 and 23 if you're really curious,) but his naive solution translates to:
if list is None:
return 1 + our_length(list.next)
This is shorter and simpler than the way I did it, but as he then points out, there's an obvious problem with this: it can produce a stack overflow if the list is long enough. In order for a functional language to cope with this problem, it needs its functions to be tail-recursive. Here's the Python equivalent of his tail-recursive version:
def rec(list, acc):
if list is None:
return rec(list.next, 1 + acc)
return rec(list, 0)
Wow! Look at that monstrosity! It's as long as my version (6 lines of code), but about twice as complicated and harder to read, requiring a nested function that does all the work for no easily-apparent reason. It's only when you understand that functional languages hate looping constructs and mutation of variables, and inflict recursion and immutability upon the developer dogmatically, rather than pragmatically allowing you to use them as appropriate, that this way even begins to make any sense at all! (Amusingly, the reason you have to write it as such a mess is so that the compiler can automagically detect that you're using tail recursion and transform the function into a loop featuring the accumulator as a mutable variable, factoring out the toxic recursion so it won't break the stack!) In reality, recursion is extremely useful when dealing with inherently recursive problems such as tree structures or divide-and-conquer algorithms, but trying to use it on linear problems where a loop is the natural fit is just asking for pain and trouble more often than not.
In summary, the reason it's difficult to convince your students that functional programming is not useless is largely because the specific flavor you're teaching them is, in fact, useless and counterproductive, an active impediment to productivity. Unfortunately, you probably can't get away from teaching the languages mandated by your curriculum, but if you can explain things in ways that are actually relevant to modern programming practice and their future careers, you'll likely see understanding dawn as they realize how and when these techniques can be appropriate. What they're missing, because dogmatic functional languages largely reject the concept, is the all-important notion of "the right tool for the job, as appropriate."