I'm Brett Slatkin and this is where I write about programming and related topics. You can contact me here or view my projects.

22 November 2022

The case for dynamic, functional programming

This question has been answered many times before, but it bears repeating: Why do people like using dynamic languages? One common compelling reason is that dynamic languages like Python only require you to learn a single tool in order to use them well. In contrast, to use modern C++ you need to understand five separate languages:
  1. The preprocessor
  2. The C++ type system
  3. The C++ template language
  4. The C++ language itself
  5. And whatever tool you're using for builds, such as make

With Python it's much easier to become proficient and remember everything you need to know. That translates to more time being productive writing code instead of reading documentation. Code that runs at compile/import time follows the same rules as code running at execution time. Instead of a separate templating system, the language supports meta-programming using the same constructs as normal execution. Module importing is built-in, so build systems aren't necessary (unless you must use a custom C-extension module). There is no static type system, so you don't need to "emulate the compiler" in your head to reason about compilation errors.
That said, Python is an imperative language, which means you still need to "emulate the state machine" to reason about how values change over time. For example, when you read a function's source code, it could refer to variables outside of its scope (such as globals or the fields of an object). That means you need to think to yourself about how those variables might affect your program's behavior by making assumptions about their values.
As a concrete example, take this stateful, imperative code:
class TimeElapsed(Exception):

class MyTimer:
  def __init__(self, threshold):
    self.threshold = threshold
    self.count = 0

  def increment(self):
    self.count += 1
    if self.count >= self.threshold:
        raise TimeElapsed

timer = MyTimer(3)
timer.increment()  # Raises

These questions might come to mind:
  • Is self.count ever modified outside of increment, and will that codepath properly raise the timer exception?
  • Is self.count reset to zero separately or should I reset it when raising the timer exception?
  • Is self.threshold constant or can it be changed, and will the timer exception be raised properly when it's changed?

In contrast, none of these questions arise in this purely functional code because all of the inputs and outputs are of known quality and the relationship between state and time (i.e., program execution order) is explicit:
def timer(count):
    if count <= 0:
        raise TimeElapsed

    return count - 1

step1 = timer(3)
step2 = timer(step1)
step3 = timer(step2)
step4 = timer(step3)  # Raises

Beginner programmers learn that they should avoid global variables because of how their hidden state and unintended coupling causes programs to be difficult to understand and debug. By the same reasoning, isn't it possible to conclude that all state that isn't strictly necessary should be avoided? Then why are we still writing such stateful programs these days?
Similar to how dynamic languages don't require you to "emulate the compiler" in your head, purely functional languages don't require you to "emulate the state machine". Functional languages reduce the need to reason about time and state changes. When you read such a program, you can trace the assignment of every variable and be confident that it won't be changed by an external force. It's all right there for you to see. Imperative languages require you to think about two modes of computation (stateless and stateful). Purely functional programs only require the former and thus should be easier to understand.
If you're a fan of dynamic languages because of their simplicity, then you should also be a fan of functional programming for furthering that goal by removing the need for thinking about state. Theoretically, functional programs should: have fewer bugs, be easier to optimize for performance, allow you to add features more quickly, achieve the same outcome with less effort, require less time to get familiarized with a new codebase, etc. With a dynamic, functional language you could enjoy all of this simplicity.
But you don't even need that to benefit from this perspective today! You can adopt the techniques of functional programming right now in your favorite language (such as Python, used above). What's important is your mindset and how you approach problems, minimizing state and maximizing clarity in the code you write.
© 2009-2024 Brett Slatkin