Functional Programming 101 Series

What Makes A Language Functional?


In this part, we try to pin down what people mean when they say functional programming language, as opposed to, say, object-oriented programming language.

A functional programming language should make it easy or idiomatic to write code without side effects. Having first-class functions is not enough to make a programming language functional. I believe that the majority of Python programmers, who are familiar with FP concepts, would agree that programming with only immutable values in Python is not sensible.

To illustrate this point, let’s compare Python to Clojure, which is one of the most popular functional programming languages. What is it that makes it a clear-cut functional programming language?

Consider a simple example of adding a new key-value pair to a map (i.e. dictionary). The most common way of doing this in Python would be a direct assignment:

some_dict = {'a': 1}

some_dict          # Out: {'a': 1}
some_dict['b'] = 2 # Out: None
some_dict          # Out: {'a': 1, 'b': 1}

From a FP perspective, this is problematic, because we have mutated some_dict and this violates the principle of working with immutable values. In contrast, the most common way to do this in Clojure would be to use the function assoc:

(def some-map {"a" 1})

some-map               ;; => {"a" 1}
(assoc some-map "b" 2) ;; => {"a" 1, "b" 2}
some-map               ;; => {"a" 1}

The difference here is that assoc creates a new map, and leaves the original map unchanged. In contrast, direct assignments in Python modify the map, which is a mutating operation. In order to leave the original map untouched, we have a few awkward options, namely dictionary unpacking:

{**some_dict, **{'b': 2}}

copying before a direct assignment:

copied_dict = some_dict.copy()
copied_dict['b'] = 2

or using assoc provided by the toolz library:

from toolz import assoc

assoc(some_dict, 'b', 2)

All three approaches are cumbersome and arguably unidiomatic.

Another source of annoyance is that there is an unavoidable performance penalty in copying the entire dictionary in the above approaches. Note that there is zero copying in the Clojure programme, because, under the hood, assoc operates on a persistent map, which shares its structures with the new map. In particular, both the original and the newly created maps share the {"a" 1} part. This is only possible, because there is a commitment to immutability in the entire language, so that structural sharing is less problematic when we know that nothing will change.

To sum up, Clojure provides an immutable-first built-in functions with good support for persistent data structures. Conversely, writing functional code in Python is neither idiomatic nor performant. For that reason, Python cannot be considered to be a functional programming language. Having said that, it should not stop us from applying FP principles when programming in Python, and reap its benefits.

Back in 2018, I gave a talk on Coconut — a functional superset of Python that transpiles to Python code. In the talk, I argued that Haskell is the gold standard for FP. This is because Haskell takes FP very seriously. Not only is it idiomatic to avoid side effects, it forces you to incorporate your side effects into its strict type system. If you are not entirely sure what that means, don’t worry so much about it! We will cover it in a future post!