| > I think this most effectively demonstrates why I like a lot of OOP: it can be verbose. Verbosity is not inherently good. In fact, I think verbosity is inherently bad. Have you read much first-year programmer code? It's absurdly verbose at the cost of legibility. The real issue is clarity. Your code should be sufficiently verbose that its purpose is self-evident, but it should not be overly verbose such that your screen is cluttered with meaningless junk (see: Java). --- To me, there is a fundamental distinction between the intents of functional programming and imperative programming that a lot of these articles either gloss over or miss completely. Imperative programming is about describing to the computer a procedure by which to accomplish a goal. Functional programming is about manipulating the relationships between data directly such that you transform your given input into the desired output. If you want to understand what a program does, then imperative code is going to be better to read. But if you want to understand what a program means, then (well-written) functional code is going to be better. This is also why so many functional languages have strong static type systems: to better enable the programmer to express programs' meanings outside of the implementation. However, as others have mentioned, string manipulation is always kind of hairy anyway, so reading this function will result in understanding what it does instead of what it means (unless you just read the signature, which is actually what I do a lot of the time). My thought process of reading this particular function without prior context would be something like: - The function is named `alignCenter` and takes a list of strings and gives back a list of strings... so the strings are being centered amongst themselves. I know they aren't being centered relative to anything else because there are no other inputs to the function — which I would not know in an imperative language, where there could be hidden state somewhere.
- It maps some function over the list of strings. I assume that function will do the centering.
- This inner function takes a string and does something to it. Let's investigate.
- We replicate a space character some number of times and prepend the resulting string to the input string. (I think the author's reliance on operator precedence is disappointing here, as it detracts from the clarity IMO. I would rather put the `replicate` call in parens before the `++`.)
- The number of spaces is determined by dividing by two some other number less the length of the string.
- That number is the length of the longest string. So: to center a list of strings, we indent each string by half the difference between its length and the maximum length among all the strings. This seems like a reasonable way to center a group of strings. (Notice that I did not use words like "first", "then", etc. which indicate a procedure. Instead, I have described the relationships of the strings.) (Of course writing it all out makes it seem like a longer process than it is, but in reality it probably took me 10-15 seconds to go through everything and figure out what was going on.) --- I think this isn't a great example for the author to have chosen. My go-to example of a beautiful functional solution is generating the Fibonacci sequence. A "good" imperative solution uses iteration and might look like: def fib(n: int) -> int:
a = 0
b = 1
while n > 1:
t = a
a = b
b = t + b
n -= 1
return b
If you were to just glance at this code without being told what it does (and if you'd never seen this particular implementation of the algorithm before), you would probably need to write out a couple test cases.Now, the Haskell solution: fib :: Int -> Int
fib n = fibs !! n
where fibs = 0 : 1 : (zipWith (+) fibs (tail fibs))
We can easily see that the `fib n` function simply retrieves the nth element of some `fibs` list. And that list? Well it's `[0, 1, something]`, and that something is the result of zipping\* this list with its own tail\* using addition — which very literally means that each element is the result of adding the two elements before it. The Fibonacci numbers are naturally defined recursively, so it makes sense that our solution uses a recursive reference in its implementation.\*Of course, I'm assuming the reader knows what it means to "zip" two lists together (creating a list by performing some operation over the other two lists pairwise) and what a "tail" of a list is (the sub-list that simply omits the first element). To me, this data-relationship thing often makes more sense than a well-implemented imperative solution. I don't care to explain to a computer how to do its job; I care to think about how to move data around, and that's what functional programming is all about (to me). Of course, this is just my subjective opinion, but I think there's some merit to it. I'd like to hear your thoughts! |
Agreed.
> In fact, I think verbosity is inherently bad.
Here I disagree. To take it to an absurd extreme: LZW compress your source code. Hey, it's less verbose! But that's not a net win.
Instead, I think that there is an optimal value of terseness. More verbose than that, and you waste time finding the point. More terse than that, and you waste time decoding what's going on.
Now, what is "optimal" is going to depend on the reader, both on their experience and their preference. With experience, certain idioms are clear, and don't require thought. The same is true of syntax. (Both Haskell and C become more readable with experience.) But some people are still going to prefer (and do better reading) a more terse style, and others are going to prefer a more verbose style.