简体   繁体   中英

Functional vs. imperative style in Python

I've been spending some of my spare time lately trying to wrap my head around Haskell and functional programming in general. One of my major misgivings has been (and continues to be) what I perceive to be the unreadability of terse, functional-style expressions (whether in Haskell or any other language).

Here's an example. I just made the following transformation in some code for work:

def scrabble_score(word, scoretable):    
    score = 0
    for letter in word:
        if letter in scoretable:
            score += scoretable[letter]
    return score

to

def scrabble_score(word, scoretable):
    return sum([scoretable.get(x, 0) for x in word])

The latter was a lot more satisfying to write (and, keeping in mind that I myself am the one who wrote it, to read too). It's shorter and sweeter and declares no pesky variables that might become a headache if I made any mistakes in typing out the code. (BTW I realize I could have used the dict's get() method in the imperative-style function, but I realized it as I was performing this transformation, so I left it that way in the example.)

My question is: despite all that, is the latter version of the code really more readable than the former? It seems like a more monolithic expression that has to be grokked all at once, compared to the former version, where you have a chance to build up the meaning of the block from the smaller, more atomic pieces. This question comes from my frustration with trying to decode supposedly easy-to-understand Haskell functions, as well as my insistence as a TA for first-year CS students that one of the major reasons we write code (that's often overlooked) is to communicate ideas with other programmers/computer scientists. I am nervous that the terser style smacks of write-only code and therefore defeats the purpose of communication and understanding of code.

My question is: despite all that, is the latter version of the code really more readable than the former? It seems like a more monolithic expression that has to be grokked all at once

I would say it's more readable, though not necessarily the most readable option. Yes, it's more logic in one line, but this is outweighed by three facts.

First, you can still mentally decompose it into smaller parts and understand those individually before combining them. I immediately see that you're summing something (at this stage I don't have to know what you're summing). I also see that you get the entries from scoretable for each letter , defaulting to 0 if not present (at this stage I don't have to know what then happens to this list). These things can be identified and understood independently. Second, there are very few somewhat-independent parts, maybe two or three. This is not unreasonable to understand at once, the "monolithic" aspect is more of a problem when dozens of concerns sit next to each other.

Lastly, the supposed advantage of the first version,

compared to the former version, where you have a chance to build up the meaning of the block from the smaller, more atomic pieces.

is also a problem: When I start reading, I see a for loop. That's nice, but it doesn't actually tell me anything yet (except that there is a loop, which doesn't help me understand the logic, only its implementation). So I have to keep this fact in mind and read on to later resolve what this loop means . To an extent, this is true of any code, but it gets worse the more code there is. Likewise, the score = 0 line doesn't mean anything on its own, it's just one tiny implementation detail that nobody needs to know to understand that this function computes a word's scrabble score by adding up the score for each of the word's letters.

When reading your imperative loop, one has to keep track of multiple mutable variables, control flow, etc. to figure out what happens, and has to piece the parts together. In addition, it's simply more characters to read, and hence it would take longer to comprehend even if understanding was instant in any case. You have to take these costs into account. Whatever you might gain by having each line be simpler is lost to this effect.

That said, the following version is arguably cleaner, precisely because it splits the two parts of the algorithm into separate lines:

def scrabble_score(word, scoretable):
    letter_scores = [scoretable.get(letter, 0) for letter in word]
    return sum(letter_scores)

But the difference is rather small, it's rather subjective. Both are fine and much better than your first version. (Oh, another thing: I'd use a generator expression rather than a list comprehension, I only kept the list comprehension to minimize noise.)

These things are primarily a matter of taste. It should be remembered that taste is itself a matter of education and experience - you find longer, imperative code more readable because that is the only thing you have learned to read (so far).

I find your second form more readable because I'm used to the form of functional style, and I understand all of the elements in it. I frequently find concise haskell code indecipherable because I haven't learned all of the operators.

The practical solution seems to me: (a) use comments (b) avoid anything excessively clever (c) explain anything clever you choose to do (d) concise and unindented is usually easier to read than voluminous and deeply indented; and (e) remember your audience.

To apply those principles to this case: in python you would expect things that can be expressed through simple functional operations to be expressed in that way, and so your audience should be comfortable with it. Your code in any case needs a comment to explain that you do (or don't) intend to silently ignore invalid (unscored) characters.

I think the concise style is nice because you are free to expand it in cases just like this one, without wasting lots of vertical space on control structures like for an if in the first. For instance

def scrabble_score(word, scoretable):
    # local function to look up a score in the table
    def get_score(x): return scoretable.get(x,0)

    scores = map(get_score, word)  # convert word into a list of scores

    return sum(scores)

I find this the most readable, even though it takes a few more lines. I'd consider this to be in the declarative style.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM