Multi-Paradigm Languages
We need to learn how to effectively use multi-paradigm languages that support functional, object oriented, and procedural paradigms.
The programming world used to be split into functional languages, object-oriented languages, and everything else (mostly procedural languages). One “was” a functional programmer (at least as a hobby) writing Lisp, Haskell, or Erlang; or one “was” an OO programmer (at least professionally), writing code in Java or C++. (One never called oneself a “procedural programmer”; when these names escaped from academia in the 1990s, calling yourself a “procedural programmer” would be akin to wearing wide ties and bell-bottom jeans.)
But this world has been changing. Over the past two decades, we’ve seen the rise of hybrid programming languages that combine both functional and object-oriented features. Some of these languages (like Scala) were multi-paradigm from the beginning. Others, like Python (in the transition from Python 2 to 3) or Java (with the introduction of Lambdas in Java 8) are object-oriented or procedural languages to which functional features were added. Although we think of C++ as an object-oriented language, it has also been multi-paradigm from the beginning. It started with C, a procedural language, and added object-oriented features. Later, beginning with the Standard Template Library, C++ was influenced by many ideas from Scheme, a descendant of LISP. JavaScript was also heavily influenced by Scheme, and popularized the idea of anonymous functions and functions as first class objects. And JavaScript was object-oriented from the start, with a prototype-based object model and syntax (though not semantics) that gradually evolved to become similar to Java’s.
We’ve also seen the rise of languages combining static and dynamic typing (TypeScript in the JavaScript world; the addition of optional type hinting in Python 3.5; Rust has some limited dynamic typing features). Typing is another dimension in paradigm space. Dynamic typing leads to languages that make programming fun and where it’s easy to be productive, while strict typing makes it significantly easier to build, understand, and debug large systems. It’s always been easy to find people praising dynamic languages, but, except for a few years in the late 00s, the dynamic-static paradigmatic hasn’t attracted as much attention.
Why do we still see holy wars between advocates of functional and object-oriented programming? That strikes me as a huge missed opportunity. What might “multi-paradigm programming” mean? What would it mean to reject purity and use whatever set of features provide the best solution in any given context? Most significant software is substantial enough that it certainly has components where an object-oriented paradigm makes more sense, and components where a functional paradigm is superior. For example, look at a “functional” feature like recursion. There are certainly algorithms that make much more sense recursively (Towers of Hanoi, or printing a sorted binary tree in order); there are algorithms where it doesn’t make much of a difference whether you use loops or recursion (whenever tail recursion optimizations will work); and there are certainly cases where recursion will be slow and memory-hungry. How many programmers know which solution is best in any situation?
These are the sort of questions we need to start asking. Design patterns have been associated with object-oriented programming from the beginning. What kinds of design patterns make sense in a multi-paradigm world? Remember that design patterns aren’t “invented”; they’re observed, they’re solutions to problems that show up again and again, and that should become part of your repertoire. It’s unfortunate that functional programmers tend not to talk about design patterns; when you realize that patterns are observed solutions, statements like “patterns aren’t needed in functional languages” cease to make sense. Functional programmers certainly solve problems, and certainly see the same solutions show up repeatedly. We shouldn’t expect those problems and solutions to be the same problems and solutions that OO programmers observe. What patterns yield the best of both paradigms? What patterns might help to determine which approach is most appropriate in a given situation?
Programming languages represent ways of thinking about problems. Over the years, the paradigms have multiplied, along with the problems we’re interested in solving. We now talk about event-driven programming, and many software systems are event-driven, at least on the front end. Metaprogramming was popularized by JUnit, the first widely used tool to rely on this feature that’s more often associated with functional languages; since then, several drastically different versions of metaprogramming have made new things possible in Java, Ruby, and other languages.
We’ve never really addressed the problem of how to make these paradigms play well together; so far, languages that support multiple paradigms have left it to the programmers to figure out how to use them. But simply mixing paradigms ad hoc probably isn’t the ideal way to build large systems–and we’re now building software at scales and speeds that were hard to imagine only a few years ago. Our tools have improved; now we need to learn how to use them well. And that will inevitably involve blending paradigms that we’ve long viewed as distinct, or even in conflict.
Thanks to Kevlin Henney for ideas and suggestions!