I really don’t think this is correct at all. Rather, Rust/Nim/Gleam are first and foremost imperative languages. They may have some functional and Lispy features thrown in, but that doesn’t change the fact that programs in those languages involve writing statements to be executed one after another — the defining aspect of imperative languages.
If you really wanted to, I guess you could define this particular combination of ‘imperative+functional+macros’ as its own new paradigm. These languages are consistent enough with their design that that might actually make sense. But it’s certainly not ‘post-paradigm‘ in any meaningful way.
There are many multiparadigm languages. They are even the norm now. But being multiparadigm doesn't mean they all support all paradigms equally. Programming X in Y is a problem for almost all combinations of X and Y for X != Y, because the language will always have a "grain" to it. Thus, it is still meaningful to argue about which paradigm preference is suitable to which tasks.
Even two of the most similar languages there are, at least in terms of language spec, Python and Ruby, demonstrate significant differences in the orientation of the library ecosystem in some of their philosophies.
Mozart/Oz and CTM are obligatory reading to understand the topic of supporting multiple paradigms: https://www.info.ucl.ac.be/~pvr/VanRoyChapter.pdf
Oz is among the few languages where all paradigms are equal. A great HN discussion about the above link: https://news.ycombinator.com/item?id=18381640
Immutable or mutable? Depends how you write it. Lists don't mutate until you choose to.
Statements or functions? Both I guess. Loop is declarative, many things are imperative, some things are functional.
Dynamic or static typing? Depends whether you bothered to give the compiler type hints or not.
Compile time or runtime? CL is AOT by default but you can eval-when your way into running code whenever you want. Readtime too
To be honest one of the biggest problems with CL is the multiparadigmatic design - you end up with many styles of code, all of which are valid and none of which anyone can agree on.
> Even two of the most similar languages there are, at least in terms of language spec, Python and Ruby, demonstrate significant differences in the orientation of the library ecosystem in some of their philosophies.
Hmm… I feel you can get a lot more similar than that. Python and Ruby are actually rather different to my mind. Rather, you could take OCaml vs SML, or Scheme vs Racket. The differences are still there, but it’s much more subtle.
There’s a perspective I find useful here. I tend to think of different ‘programming paradigms’ as different approaches to solving problems. In imperative languages, you solve problems by modifying values in sequence until you get to the answer; in functional languages, you solve problems by building up a function which gives you back the answer; and so on.
The key thing here is, you do need to give yourself some way of solving problems. That translates directly to the paradigm which your programming language adopts. If you give yourself more than one way of solving problems, then the language becomes multi-paradigm. In rare cases, you might even end up inventing a totally novel problem-solving approach, and hence a new paradigm… but even that’s not ‘post-paradigm’, it’s just another paradigm.
I don’t disagree on any of your technical points. But I also think for practical purposes you’re missing the forest here. I agree with the sentiment of the article - I think the big trend in general purpose PLs is a blend of multiple classical paradigms. Perhaps we’re moving the goalpost and paradigms need to be rearranged - but that is intrinsically interesting - it’s literally the continents of knowledge drifting slowly into new configurations.
Single-paradigm languages like prolog, CSS or SQL keep their restrictions not because the lack of use-cases, but because the benefit of keeping the complex execution engines away from the end-user exceeds the minor wins in expressiveness.
I don’t think it’s a coincidence that the declarative languages are in this category. They are higher level, and opening up low-level customizations is really tricky: for instance, if you put imperative code inside your CSS, it needs complex “re-evaluation rules”, that results in a dilemma: either give full control to the programmer, which imposes specific execution engine designs and complex API surfaces – or re-evaluate too often, which risks killing memoization and perf (cache invalidation). It could be even worse if the code has side-effects or dep cycles.
On the contrary, imperative low level languages like Rust can easily come along and say things like: “this is not only a function, but a side-effect free function”. “This is not just a reference, but an immutable reference”. Then you can cleverly leverage those traits in your “execution engine” ie the compiler, to deliver low-level perf. There are even people who describe rust as a high-level language for these reasons, which is a bit provocative to me but in all honesty not completely outrageous.
An alternative take on the last 10-15 years:
- General purpose PLs typically have an imperative base, while integrating multiple classical paradigms:
- Only a few aspects of OOP are added to modern PLs, where inheritance has largely been superseded by simpler composition
- Features from FP have surged in popularity, being integrated and even retro-fitted into general purpose PLs, providing both perf- and DX improvements
- Structured meta-programming and/or codegen has been a strong focus for compiled languages, acknowledging that it’s preferable to limit the complexity of the core language at the expense of separate pre-compile phases
Gleam is one of the really really functional languages out there. There is no mutability, there are no statements, no loops. It has let bindings, function application, and conditionals through if/case, and that’s it. It has just tricked you because it has a very nice and friendly syntax.
It also has no macros so I don’t know what you mean in that last paragraph.
If I write a program, fundamentally I have a blob in my head that I'm reifying into formal logic. It doesn't make sense to talk about the blob as having a paradigm; but I'm not sure that it is useful to talk about the reification as having a "paradigm" either. The appropriate techniques to be used are on a functional-imperative spectrum based on how stateful the problem is.
This whole idea of programming paradigms I think is a mis-take of a more fundamental question of how to classify programming problems - we're looking at the shadow in Plato's cave and getting nonsense because we expect the problem to to take on aspects of the programming language. Which is not a strategy for achieving success. We should be classifying problems based on state, not programming languages based on paradigms.
Most of the experts have figured that out, they pick their programming language based on the problem space they want to deal in. But the paradigm paradigm doesn't help in making those decisions, so it's utility is low.
Certainly all three languages can be used in a simple, imperative style. But that's not the only paradigm that can be used unlike in C or early versions of Python. Many programs in these languages look significantly different than just statements and procedures.
In my experience, Rust traits are used much more for static dispatch `fn foo<T: Trait>(T)` than dynamic dispatch `fn foo(Box<dyn Trait>)`. This, together with its ADTs and lack of inheritance, gives it a very different feel from most "object oriented" languages.
Also, what counts as a "defining feature" depends greatly on who's defining it. Though dynamic dispatch is certainly up there on most lists.
Like I said, they’ve certainly adopted features from other paradigms… but the basic, underlying structure of programs in these languages is still statements, sequenced one after another. It’s not like Haskell or Scheme where nearly every operation is ultimately done through function calls. And it’s certainly not ‘post-paradigm’ as the article claims.
> Moreover, all three languages support structural pattern matching (borrowed from functional programming) as a core control-structure.
As for this: people often associate functional programming with pattern-matching, but I’ve never really understood why. The only relationship is that it originated in the ML language family, which also happens to be functional. There’s many functional languages which lack pattern-matching (e.g. most Lisps), and there’s many non-functional languages which have it (Java, C#, Python).
At minimum, that means there are _two_ paradigms: logic programming and "beyond paradigms"....which sorta means that we're not really beyond paradigms now, are we?
I think the reality is that certain previously-niche paradigms became cool again, and folks started grabbing off some of the most-visible pieces of those paradigms (pattern-matching, ADTs, map/reduce/filter, maybe even a pipeline operator) and adapting them to other paradigms.
In Metamine
a := b is a normal assignment
c = d+1 means that c will ALWAYS be equal to d+1
z = 10-time results in a countdown timer, z
That magical equals is declarative programming... something that I've only seen mixed with imperative functioning that one time.It's also pretty much the same as a dataflow constraint (see: Amulet, Garnet, Spreadsheets, ...)
In Objective-S, I use the syntax |= for the unidirectional variant of a dataflow constraint, =|= for bidirectional (ObjS uses := or ← for assignment, = for equality). So the above would be:
a := b.
a ← b.
c |= d+1.
z |= 10-time. (if 'time' were the current time in ObjS)
https://objective.stGot closures? Then you've got objects.
Can you write code that's executed sequentially? Then you've got imperative programming.
Can you pass a function as a parameter to another? That's pretty functional.
Having this in a language has been common for decades. But there are more paradigms, notably 'macro-oriented' programming and logic programming. Are these well supported in Rust and Nim? If not, then they aren't as "beyond paradigms" as Lisp.
There's arguably several forms of object oriented languages too, besides what might be called Java/C++/Python style there's message passing Smalltalk style and prototype style as in JavaScript and its parent. Would probably need to support these styles too, to be "beyond paradigms".
Assembler could also be considered a programming paradigm, so we'd need to support that too.
And if logic programming is a paradigm, constraint programming likely is another. Glue or shell programming yet another.
And so on and so on.
Personally, I'd love to see us talking more in terms of problems/solutions.
To give an example. State leads to complexity and bugs, and it can be minimized by preferring pure functions where possible. That's a good principle that can be learnt from without embracing everything functional and rejecting everything that involves objects.
Greenspun’s Tenth Rule always irritated me more than it amused me but here we are.
Javascript: f(g(x)) Lisp: (f (g x))
Same amount of parens.
Since Oz's Wikipedia page was linked, Wikipedia apparently has a page (https://en.wikipedia.org/wiki/Comparison_of_multi-paradigm_p...) comparing multi-paradigm languages (though can be argued all languages nowadays are multi-paradigm). A summary table for some of those (partial support isn't considered):
| Paradigms supported |
Language Native Libs Others Total
Wolfram 14 0 1 15
Oz 11 0 0 11
Raku 10 1 3 14
Racket 10 0 1 11
Julia 9 5 3 17
Scala 9 0 0 9
Haskell 8 5 2 15
Perl 8 1 0 9
CLisp 7 8 2 17
C++ 7 7 1 15
Python 6 4 0 10
Java 6 2 0 8
Rust 6 0 1 7
JS 4 3 2 8
Fortran 4 1 0 5
OCaml 4 0 0 0
Go 4 0 0 0
Lua 3 0 0 0
Interesting ones will be Wolfram supporting the most basic (not in others) natively, CLisp the only one supporting all the basic considering libraries, and Julia that alongside CLisp support the most considering extra (in others). Moreover CLisp alongside C++ show that thanks to libraries supported can be greatly extended (doubled in those two). To also compare to article, another will be Haskell supporting more paradigms (even only considering native ones) than Rust. Sadly Nim and Gleam aren't in list.The author states "I believe that programming paradigms are now best understood as a style of programming, rather than as an exclusive set of features." which is not correct. A "Style of Programming" is not a "Paradigm" unless that style embodies a specific computation/abstract machine model. The relevant syntactic features could be used in a mix-and-match manner to increase the design expression space. A good example is the template features in C++; originally designed as an alternative to macros to implement containers until some smart people figured out that it was much more powerful and could be used for programming in the "Functional Paradigm" (with some additional extensions).
OTOH Java has a defined language standard (independent of implementations), which describes classes/fields/methods/dynamic dispatch/inheritance/hiding/constructors, ... ( https://docs.oracle.com/javase/specs/jls/se22/html/jls-8.htm... ).
I fail to find a similar language definition of OOP for Wolfram. That could be me.
As other commenters are saying, in 2024 we can combine paradigms more than before the 2000s but paradigms continue to exist. It is very different to use think in functional terms than in imperative ones or even in purely logic terms as in Z3. There are combinations that are pretty natural (e.g. LINQ in .NET) while others go beyond specific languages: just interfacing components written under different paradigms.
My two cents is that programming languages will always favor a paradigm over others because many paradigms are strongly connected to the compiler or interpreter and offer syntax sugar to give the developer different strategies. The multiparadigm approach would much alike the lines of Microsoft .NET (F#, C#) where specific programming languages can use a unified framework.
In futuristic terms I think the whole programming field will majorly lend more toward reuse and developing based on specs than what we are doing now: reinventing the wheel across different organizations. For example, front-end development should be more visual and parametric than the code it requires to write.
And, we should also think about the way that computer science and/or software engineering is studied. If there were a better teaching approach about multiparadigm thinking there would be less cognitive load. My argument is that when you work professionally you don't spend the same cognitive requirements that studying at university because even unconsciously you assume that you know most of the material.
What I agree is that you would not find many people who are profficient in Haskell/OCaml/Lisp at the same time of imperative languages. You can know both but it is rare to work interchangeably in both. Again, I think that part of this is how we learn computer science and that new pedagogic ways could help to be at least good in both.
My personal frustration with logic implementations like Prolog is that the promise on focusing in the "what" instead of in the "how" is not fullfiled. I think that SMT solvers like Z3 are great in this topic at the expense of narrowing the problem space.
Imperative programming lets you think about a series of steps. Structured programming lets you split and name series of steps into known procedures and structures. Object oriented programming (and object relational mapping) was wildly successful because a lot of enterprise programming is about interactions between durable real world entities. Functional programming remains somewhat niche because modelling in terms of pipelines of pure functions is hard to map to real world problems without great discipline.
None of these is right or wrong, nor is any subset or combination. But they’re not just regrettable constraints, they’re valuable ways of thinking. Any language seeking to grow beyond one or more paradigms still needs to offer a clear and consistent way for programmers to reason about the world.
Just take a look at how Python is evolving, and the trail of essentially “dead” features and standards it’s leaving behind.
An example is dependent types. This is impossible in all the languages the author mentions in the article (yes, well, GHC Haskell comes close), but there is at least one "21st century general-purpose language" that uses it, called Idris.
Programming paradigms can't be defined intensionally, by listing their features, nor extensionally, by listing programming languages that fit each paradigm. Either way results in unsatisfactory definitions from which reasonable people will identify errors of excluded languages.
This is because programming paradigms aren't sets, they're cognitive categories. Programming paradigms are subjective mappings of conceptual framings of program structure onto specific languages. A language fits a paradigm if that mapping is subjectively natural.
Of course, people tend not to like subjective definitions, but this is more accurate.
A better viewpoint is to question what sort of problems or way to model a system, is a particular paradigm best suited to?
"What kinds of music do you have here"
"Oh we got both kinds: Country and Western"
(Blues Brothers, https://www.youtube.com/watch?v=vS-zEH8YmiM)
Let's see:
f(x) // procedural, side effects discouraged
x.f() // OO, the procedures are scoped/namespaced/grouped
f(x) // functional, side effects disallowed
OK, that's a little facetious, but only a little. More details here:https://2020.programming-conference.org/details/salon-2020-p...
So yeah, we need to go beyond this.
https://blog.metaobject.com/2019/02/why-architecture-oriente...