Towards functional programming

A conversation on how paradigm shifts (don't) happen

The following is a hypothetical conversation that could happen if a person working in a team were to suggest a functional programming approach in an ecosystem where this is not the norm. Its purpose is to explore the different sides of such an issue in a somewhat Socratic manner and to ultimately show how the established ways of doing things in software engineering communities are difficult to change.

A: Guys, I think we should start writing functional Python.

B: What exactly do you mean by this? Using filter, map and reduce or something?

A: No, I’m talking about abiding by functional principles as opposed to merely using functional features. You know the functional principles, right?

C: Yes, immutability, referential transparency, pure functions, minimising state.

A: Yes. Even though Python itself doesn’t enforce those principles, there is no reason we can’t choose to abide by them ourselves. If we are disciplined enough we can reap very similar benefits to those we would had we used a purely functional language.

B: What benefits are those?

A: Well, for instance robustness, ease of testing and refactoring, better maintainability and scalability.

B: These are just the same benefits pretty much everybody says any old good practice has: test driven development, design patterns, microservices, whatever.

C: I mean, these are not all mutually exclusive, we could use several of those. But also, those ideas are not all in the same category. For one, functional programming is a programming paradigm.

B: Sure, I know, I’m not saying they’re the same. I’m just saying those are ideas many people consider important and worth doing. So why is functional programming more important and worth doing?

A: Well I think that’s pretty intuitive. Do you not see how less state is better? How immutability makes for less bugs? I mean, that’s what all bugs are after all - the unpredictability of your code’s behaviour. Functional programming, even in Python, can minimise this unpredictability drastically, or at least isolate it to just certain places in code where you can clearly see it and work with it. Comparing those other ideas you mentioned to functional programming is like comparing taking good care of your house once it’s built to actually building strong house fundamentals so that it needs less taking care of in the first place!

B: I see you’re very passionate about it, but to me it’s just not that intuitive. I mean I can kind of see your point but I don’t understand why it is such a big deal.

A: Well if you don’t understand why it’s such a big deal than sorry but you haven’t really understood my point.

B: Yeah see, this is why people say FP guys are elitist and a cult.

C: Guys stop, this isn’t helpful. What people say doesn’t matter. What matters is the practical reality of our work. I think you should at least provide a practical example before we start arguing.

A: Sure, take a look at this functional refactor of a class I wrote:

#ORIGINAL CLASS

class Coffee:
    def __init__(self, name, price):
            self.name = name
            self.price = float(price)

    def get_change(self, budget):
            return budget - self.price
    
    def sell(self, budget):
            if budget < 0: 
                print('Sorry you don\'t have money') 
                exit() 
            if budget >= self.price:
                    print(f'You can buy the {self.name} coffee')
                    if budget == self.price:
                            print('It\'s complete')
                    else:
                            print(f'Here is your change {self.get_change(budget)}$')

                    exit('Thanks for your transaction')


small = Coffee('Small', 2)
regular = Coffee('Regular', 5)
big = Coffee('Big', 6)
 
try:
    user_budget = float(input('What is your budget? '))
    if not isinstance(user_budget, (int, float)):
        print('Enter float or int')
        exit()
except ValueError:
   exit('Please enter a number')
  
for coffee in [big, regular, small]:
   coffee.sell(user_budget)
   
#FUNCTIONAL REFACTOR

def buy_coffee(budget):
    match budget:
        case _ if budget < coffee_prices['Small']:
            print('You cant buy coffee')
            return budget
        case _ if coffee_prices['Small'] < budget <= coffee_prices['Regular']:
            print('You can buy a small coffee.')
            return budget - coffee_prices['Small'] 
        case _ if coffee_prices['Regular'] < budget <= coffee_prices['Big']:
            print('You can buy a regular coffee')
            return budget - coffee_prices['Regular']
        case _ if budget > coffee_prices['Big']:
            print('You can buy a big coffee.')
            return budget - coffee_prices['Big']
    

coffee_prices = {'Small':2.0, 'Regular': 5.0, 'Big': 7.0}   

try:
   user_budget = float(input('What is your budget? '))
   change = buy_coffee(user_budget)
   print('Your change is {}$. Thank you!'.format(int(change)))
except ValueError:
   exit('Please enter a number')


C: This does look pretty elegant. But it’s just a little example. I worry what would happen when you try to design an entire real-life codebase this way. It would have no shape. Without object-oriented design there wouldn’t even be any abstractions, just a bunch of functions everywhere. I don’t think it would be very readable.

A: First off, Python does support non-object-oriented abstractions: most notably we have decorators, but also things like Enums and dataclasses, provided we use them only for data, not behaviour. That being said, I’m not trying to argue that Python is better than other languages for following FP principles. But I like to argue from the Python perspective because wider FP adoption is important to me and given Pythons’s rising popularity I think it’s a good way to reach a lot of people. Also, Python’s imperfections are actually useful to me, as I’m trying to demonstrate precisely the fact that even in such a language we can follow FP principles well enough, and following them well enough is better than not following them at all. In fact, if every Python developer followed them even a little more, this would, little by little, amount to a large net improvement of many of the world’s codebases.

C: You trying to change the world is cute, but you’re not answering my question. How would you extend this example to an actual real-life codebase?

A: Don’t condescend me, I’m not just an idealist. I’ve actually given the practice of this a lot of thought. First off, I think just using modules and files as architectural units is underrated. If the only benefit of a class is just to bundle some related functions together, then why couldn’t a well-named file in a well-named module containing well-named functions do the trick? We could also introduce a new convention such as every module containing a ‘utils.py’ file for functions and ‘data.py’ for data structures to enforce the separation of actions, computations and data. Then actions could go into ‘main.py’. Would this not give our codebase enough structure, while also abiding by FP principles?

image

B: Wait, what exactly do you mean by ‘the separation of actions, computations and data’?

A: I’m referring to Eric Normand’s definition of functional thinking, which is basically a way of abiding by FP principles in a non-FP language. He’s written a book about it called Grokking simplicity. Although his examples are in Javascript, the concepts he presents are language-agnostic. He explains it better than I could have.

C: This seems to be a pretty well-known book. If it were truly applicable to Python, the community would already be talking about it.

A: Well, I’m talking about it. I mean, someone has to be the pioneer. Also, there are practical reasons why the Python community might be vary of FP. For example, a lot of people using Python today are either relative beginners, data scientists and other types of scientists, or simply very pragmatic engineers who got into Python precisely because it’s very quick to learn and develop. All of those are often not the kind of people who find joy in learning computer science concepts such as referential transparency and higher-order functions.

B: Well, if the Python community doesn’t want to do FP, then why force it. Let’s just stick to the established ways of doing things: object-oriented programming and design patterns. This has been the standard since the very beginnings of programming for a good reason.

A: Yeah? What’s the reason?

B: I mean I’m not sure, but I’m sure the established experts know what they’re doing.

A: This is so near-sighted. If everybody thought this way no innovation would ever happen. We wouldn’t even have computers if everybody used this thought-terminating cliche.

C: I understand that you’re all about innovation and going against the current and I guess that’s admirable. But I just don’t think we should use FP in our project just to be innovative. We have to decide based on practical benefits.

A: But I have talked about practical benefits! There will be less unexpected behaviour. Our unit tests will be super simple to write. The code will be much easier to parallelise. If we are disciplined about naming conventions and type hints it will also be much more readable.

C: Those are good points. However, I still worry about doing something the community at large isn’t doing. For example, all of the external libraries we’ll be using will have an object-oriented API and we will have to work around it.

A: We can do that. It’s not going to be pure in any sense of the word, but it will still be good code. FP won’t be something that limits us, but it will still be something that guides us. We will still harness its power.

B: I’m just not convinced that it’s worth it. I mean, it it’s gonna be impure anyway then why do it at all, it’s just messy and difficult. It sounds like we’re gonna have to bend over backwards for this just because you say it’s better.

A: It’s not because I say it’s better! I have given you tons of arguments for why it’s better!

B: Well I don’t see it. Or maybe it’s just not that important to me. Do you really have to overthink it? Let’s just write code.

A: You are exactly what’s wrong with software engineering. Just writing code without ‘overthinking’ it is what leads to bad software. All engineering worth doing is ‘overthinking’. I’m sick of this discussion, you’re so small-minded.

B: And you are rude and conceited. If I don’t want to think about stuff more deeply you can’t make me!

C: Now hold on guys… I do agree that we can’t ‘just write code’, we have to do some design beforehand, whether it be functional or not. Can’t we compromise on this instead of fighting?

A and B have already left the room and can no longer hear C.