Dev.to · 13 min read

Functional languages are heavily imperative.

Functional languages are heavily imperative.

This article will seem full of contradictions. Even writing it feels like a contradiction: Why am I digging through academic Haskell papers from the '90s right in the middle of my AI psychosis moment? Literally, as I write this, I am running my first experiment with an agent delegating tasks to another agent. So why am I spending time on esoteric academic nonsense instead of giving in to the vibes and building my loopy graphy software factory? I am writing this because AI is a nuclear amplifier on the patterns we establish in our codebases, and we literally do not have the right terminology to establish scalable patterns. Nobody has the answers we need because nobody even has the right terminology to talk about it. Two Dimensions, Not One Industry dogma says that functional and declarative are synonyms—that code is either side-effectful and imperative, or pure and declarative: But functional and declarative are independent dimensions: Code that is both functional and imperative debunks this, and so does code that is both side-effectful and declarative. Both exist! "Imperative Functional Programming" Simon L Peyton Jones and Philip Wadler helped create Haskell, which is a beautiful, pure functional programming language. In 1993 they wrote a paper called "Imperative Functional Programming." The title may sound like an oxymoron, but it isn't. The functional programming community had been struggling with how to manage side-effects like I/O, and these co-creators of Haskell proposed a way to decouple the expression of I/O commands from their execution. But they noticed something: It will not have escaped the reader’s notice that programs written in the monadic style look rather similar to imperative programs. For example, the echo program in C: echo() { loop: a = getchar(a); if (a == eof) return; else { putchar (a); goto loop; } } In Haskell: echo :: IO () echo = getcIO 'bindI0' \a -> if (a == eof) then doneIO else putcI0 a 'seqI0' Does the monadic style force one, in effect, to write a functional facsimile of an imperative program, thereby losing any advantages of writing in x functional language? Nope! Jones and Wadler list two advantages that remain: List operations (like mapand append) can still operate on monadic commands Effects can be expressed without immediately executing them. "It's a bit like being able to define your own control structures in an imperative language." Recognizing the imperative structure of the Haskell monadic pattern does not mean we have to abandon the advantages that are inherent in pure functional programming. But it is still imperative structure. "Lazy Imperative Programming" John Launchbury, another creator of Haskell, wrote a paper the following year called Lazy Imperative Programming. In it he says, Imperative features were introduced to Glasgow Haskell for expressing input and output. Could this be plainer? Haskell has imperative features. And the Haskell code that uses them is imperative. Also from his paper, an "Imperative Scan Left": writeVar is an imperative statement, which is why this is an example of imperative code—written entirely with pure functions!!! The paper has many other references to imperative stuff, including this: In a strict imperative framework such as the IO monad (and most imperative languages), no value could be returned until the whole of the list was traversed. Using lazy sequences, however, this is not the case. If only the head of the list is required then very little of the computation is performed: the variable is allocated and initialised, it is read, and the list returned with that value in the head. If even less is required, merely whether the final list is empty for example, then the variable is not even allocated as only xs needs to be examined in order to give the structure of ys. 🤯 This is insanely cool. Again, not everything in Haskell has to be declarative for it to still allow for incredible execution properties! The "Imperative Feel" of Concurrent Haskell Paul Hudak (another creator of Haskell) and Conal Elliot wrote a paper in 1997 where they weren't happy with the "strongly imperative feel" of Concurrent Haskell—even though it was 100% purely functional. (Side note: I owe my passion for programming to this paper. More on that later.) Here's an example of what that pure functional approach with an "strongly imperative feel" looked like: box 42 Just a single declaration. Nothing here expresses a command at all. putMVar is 100% imperative, while this is 100% declarative and 0% imperative. Note on "more declarative" and monoparadigmatic dogma Note on "more declarative" and monoparadigmatic dogma Since the industry dogma in the 1990s was "functional = declarative", they had to call FRP more declarative. Concurrent Haskell was already declarative, supposedly, by virtue of being in Haskell—so this had to be extra declarative 🤷 And I say that with great admiration for these authors, because I owe my passion for programming to this paper. This was the origin of both RxJS and signals, which I love... very much. Too much for many people. The "Lazy Imperative Programming" paper was more contradictory. The author often contrasted Haskell with "imperative languages," while dozens of times referring to features within Haskell as imperative. On the one hand, functional languages are commonly more expressive and easier to reason about than imperative languages, After reading 20 or so papers from the 1990s talking about programming paradigms, you will understand that the terminology was extremely entrenched. Academics classified programming languages into rigid categories to help explain the programming patterns of the time. Functional programmers were especially guilty of reinforcing this reductionism, since they were a minority of programmers and had to fight against the grain with every rhetorical tool they could. Branding functional programming languages as automatically 100% declarative was an enticing selling point—even though we have many Haskell creators very plainly referring to features within Haskell as imperative. Also, in the 1990s very few "imperative languages" had added much functional features, having not had to deal with much concurrent processing yet, which meant languages really did mostly fall into neat categories; multi-paradigmatic languages were not the norm like they are now. The concept of entire languages as "imperative" or "declarative" somewhat applied back then, but today is a nothing but a leftover from decades of synchronous code that all matched the same 2-3 structural patterns. We have to make a mental note of this history, but if we do not push to correct the terminology, it will continue to confuse developers and AI. Many developers think that stuffing imperative features between < > characters magically makes them declarative because it's part of HTML, a "declarative language." Some are attempting this and creating masses of spaghetti code that only cosmetically looks different from the imperative JavaScript spaghetti they were writing last year. There are no "declarative languages" and "imperative languages", as much as it would simplify selling them. There are commands expressed in code, which are imperative by the purest definition; and there are features and behaviors declared without scattered commands controlling them from elsewhere. Conclusion (Imperative Functional Programming) The literal precise English definition of "imperative" is something that expresses a command. That is what these pure functions are doing in Haskell and other functional languages. This should already rest the argument. But even the less precise definitions of imperative programming focus on structural descriptions written in code rather than execution details: "Imperative programming involves writing programs as sequences of explicit commands that are executed in order from top to bottom." https://builtin.com/articles/imperative-programming "logic encoded as a sequence of ordered operations" https://en.wikipedia.org/wiki/Programming_language "Imperative programming focuses on describing how a program operates step by step, rather than on high-level descriptions of its expected results" https://en.wikipedia.org/w/index.php?title=Imperative_programming "With an imperative approach, a developer writes code that specifies the steps that the computer must take to accomplish the goal." https://learn.microsoft.com/en-us/dotnet/standard/linq/functional-vs-imperative-programming "Im­per­a­tive pro­gram­ming languages are composed of step-by-step in­struc­tions (how)... By contrast, in de­clar­a­tive pro­gram­ming, the desired result (what) is described directly." https://www.ionos.com/digitalguide/websites/web-development/imperative-programming/ Imperative programming is a paradigm where you explicitly state how the program should achieve the desired result. https://octopus.com/devops/infrastructure-as-code/declarative-vs-imperative-programming/ It's very clear that Haskell and functional programming languages are extremely cool with the flexibility they afford for all kinds of things, including the execution of side effects. But Haskell has features that enable writing literal imperative commands even with pure functions. This results in code that is structured imperatively: Explicit commands incrementally adding to behavior described elsewhere. The instant a single command appears, it completely takes away the declarative quality of describing the final result of something up-front. The imperative features of Haskell are not just less declarative; they are completely anti-declarative. Functional programming can be imperative. Declarative Side-Effects RxJS RxJS enables fully declarative code, despite latent side-effects. RxJS actually came from that Fran paper we looked at earlier, which proposed a way to turn this kind of Concurrent Haskell: box 42 Neither of these trigger side-effects, but the declarative version structures the code as a single description of a complete, final logical result (a behavior over time). The thing that made the first imperative and the second declarative is the structure of the code, not the side-effect execution model. The same structural move can be made in side-effectful code: const box = Promise.withResolvers(); setTimeout(() => { box.resolve(42); }, 1000); const result = await box.promise; const box = await lastValueFrom( timer(1000).pipe(map(() => 42)), ); Both of these trigger side-effects when executed, but the declarative version structures the code as a single description of a complete, final logical result. The thing that made the first imperative and the second declarative is the structure of the code, not the side-effect execution model. These four code snippets alone are enough to complete our four quadrants: But let's look at some more common declarative side-effects. HTML "HTML is declarative" is often said. And it's mostly true (see above callout "Note on "more declarative" and monoparadigmatic dogma"). Yet the entire point of HTML is a side-effect: You being able to look at beautiful interfaces like this: It's not just the DOM that's a side effect though. The HTML rendered in the page can itself trigger other kinds of side effects, like network calls: Nondeterministic Declarations Math.random Math.random() technically produces a side effect because it relies on and mutates a hidden global internal state (the seed or PRNG algorithm pointer) inside the runtime environment to generate its next value. But this is still totally declarative: const x = Math.random(); Date.now(), crypto.randomUUID(), process.memoryUsage(), etc... const now = Date.now(); const date = new Date(); const perfNow = performance.now(); const randomBytes = crypto.getRandomValues(new Uint32Array(1)); const uuid = crypto.randomUUID(); const hrtime = process.hrtime(); const uptime = process.uptime(); const memoryUsage = process.memoryUsage(); const freeMemory = os.freemem(); const battery = await navigator.getBattery(); const windowWidth = window.innerWidth; const visibility = document.visibilityState; const online = navigator.onLine; All of these are completely declarative. They are declarations of final results, and no subsequent step-by-step (or any) imperative code is involved. But they are not pure functions. They don't have side effects, but they have side causes, so they still are not functional. So while they aren't examples of declarative code that causes side effects, they are examples of declarative code that break rules of functional programming, showing that these are independent dimensions. Declarative "Pure" Functions with Side-Effects CPU const fib = (n: number) => n < 2 ? n : fib(n - 1) + fib(n - 2); const result = fib(50); Everything here looks pure as the driven snow, and it's definitely declarative. No side effects at all, right? In fact, it triggers 40,730,022,147 calls, which means that its side effects are so severe that I really don't recommend pasting that into dev tools and running it. You think jQuery has interesting side effects by reaching into the DOM and changing the behavior of random things, but this will change the behavior of everything on the page. Namely, it eliminates it because it locks up the CPU. Memory new Array(2 ** 30).fill(0); No side effects here, right? It's declarative, at least. However, this might get compiled into assembly code that looks like this: mov rdi, 8589934592 call malloc mov rcx, 1073741824 mov rdi, rax xor eax, eax rep stosq There is a huge side-effect in this code: call malloc requests 8589934592 = (2 ** 30) × 8 bytes = 1,073,741,824 × 8 bytes ≈ 8 GiB of memory. There may be guardrails against creating arrays this big. I didn't feel like testing it. But there is a variation that may not be prevented: Array.from({ length: 1000 }, () => new Array(1_000_000).fill(0)); This time each inner array has only 1 million elements, but there are 1000 of them, so 1,000,000,000 total elements. Here are some possible side-effects: The page or Node.js process becomes unresponsive while the runtime tries to allocate and initialize the array In a severe case, the browser tab crashes because it exceeds its memory limit. And yet, it's still declarative. Conclusion (Declarative Side-Effects) Declarative and functional are not synonyms. Declarative code is about how a computation is structured: describing a result rather than spelling out a sequence of commands to get there. Functional purity is about what that computation depends on and affects. A declaration can trigger network requests, consume CPU and memory, observe clocks and runtime state, or depend on hidden mutable state without becoming imperative. These are separate dimensions. Conclusion The point of separating these dimensions is not to win a pointless terminology argument. It is to make better engineering decisions while AI is applying incredible pressure on our architectural patterns. If we collapse functional into declarative, we lose the ability to talk precisely about what code is doing. Pure code can still be structured imperatively. Declarative code can still observe mutable state, trigger I/O, allocate absurd amounts of memory, or otherwise affect the world. Structure and purity are independent properties of code that sometimes align, but DO NOT HAVE TO! If the problem is hidden state, surprising effects, or difficulty reasoning about dependencies, functional programming can help with its explicit inputs, immutable values, pure transformations, effect isolation. And although I love functional programming, in my experience, most apps genuinely will never see significant consequences for not being purely functional. However, most apps I have seen have suffered severely from being imperative: It always scatters control, with commands reaching across a system (spaghetti code), or behavior that can only be understood by mentally executing twenty steps across multiple contexts. Declarative code fixes this. Functional programming does not. I am sick of saying I like declarative code and being told to juST UsE ELm or HASkElL. People can't even understand the problem because they don't have the words to discuss it properly. Now throw AI into this cross-talking mix of contradictions, and this is what we can expect: This looks delicious, but can we please try to avoid it anyway? Let's fix our vocabulary so we can learn real things about imperative code and steer humans and AI away from it.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News