Strange Loop Conference
Compilers can outperform imperative languages on mutation-heavy workloads by using static alias analysis to execute in-place memory updates without violating pure functional immutability.
AI-assisted summary of "Outperforming Imperative with Pure Functional Languages" by Richard Feldman — original source. Check the original for full context.
Source published:
Key ideas
- Baseline language mechanics often eclipse paradigm differences; rewriting NoRedInk's primary backend service from Ruby on Rails to compiled Haskell produced a 10x performance boost. (2:08)
- Roc compiles to native binaries using LLVM, eliminates boxing overhead, and uses compile-time static reference counting to reclaim memory without garbage collection pauses. (5:04)
- Pure functional languages eliminate managed effect penalties by keeping tasks and closures unboxed and applying defunctionalization to replace dynamic function pointers with direct calls. (10:21)
- By applying opportunistic in-place mutation via the Morphic alias analysis solver, Roc's purely functional Quicksort ran faster than Java, JavaScript, Go, and Haskell, trailing only C++. (28:43)
Detailed notes
## Background Delivered by Richard Feldman, software engineer at NoRedInk, this talk was recorded at Strange Loop 2021 and published on YouTube by the Strange Loop Conference channel on October 20, 2021. According to the description, Feldman examines how and when pure functional programming languages can overcome common performance hurdles to outperform imperative counterparts from high-level network logic down to CPU instructions.
## TL;DR Feldman challenges the conventional belief that pure functional programming is inherently slower than imperative programming, arguing that runtime execution models, compilation strategies, and boxing overhead dominate real-world performance far more than paradigm differences. He highlights how managed effects can be made virtually cost-free using unboxed closures and defunctionalization, while immutability can either unlock fast structural checks or be compiled away entirely. Focusing on Roc, a native language inspired by Elm, Feldman explains how compile-time static alias analysis via the Morphic solver enables opportunistic in-place mutation. This compiler optimization allows a purely functional implementation of quicksort to execute in-place swaps under the hood, outperforming imperative implementations in JavaScript, Java, and Go, and finishing close behind C++.
## Atmosphere & style Feldman presents with an engaging, lucid, and pragmatic engineering mindset, interweaving technical rigor with approachable storytelling and self-deprecating humor. He avoids dogmatic evangelism, speaking transparently about benchmark limitations, the realities of production engineering at NoRedInk, and the experimental status of Roc's static reference counting. The talk feels like an exciting, demystifying tour of compiler mechanics, building palpable momentum as low-level optimizations systematically overturn long-held assumptions about functional language performance.
## Bulletin - [02:08](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=128s) Baseline language mechanics often eclipse paradigm differences; rewriting NoRedInk's primary backend service from Ruby on Rails to compiled Haskell produced a 10x performance boost. - [05:04](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=304s) Roc compiles to native binaries using LLVM, eliminates boxing overhead, and uses compile-time static reference counting to reclaim memory without garbage collection pauses. - [10:21](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=621s) Pure functional languages eliminate managed effect penalties by keeping tasks and closures unboxed and applying defunctionalization to replace dynamic function pointers with direct calls. - [28:43](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1723s) By applying opportunistic in-place mutation via the Morphic alias analysis solver, Roc's purely functional Quicksort ran faster than Java, JavaScript, Go, and Haskell, trailing only C++.
## Key takeaways - [00:06](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=6s) Richard Feldman created Roc to bring the ergonomic, pure functional experience of Elm outside the browser into standalone native binaries. - [02:08](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=128s) Runtime execution models and language overhead—such as interpreted ASTs versus LLVM-optimized binaries—dominate speed far more than the functional versus imperative paradigm divide. - [02:08](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=128s) Value boxing places primitive types and records onto the heap behind pointers, adding severe indirection and memory pressure in dynamic and managed languages. - [05:04](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=304s) Roc utilizes static reference counting to cancel out increments and decrements at compile time, retaining the latency benefits of ARC without tracing GC pauses. - [08:03](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=483s) Pure functional systems model side effects as managed effects: inert data structures describing tasks that the runtime executes, structurally similar to JavaScript Promises. - [10:21](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=621s) Managed effects avoid runtime bottlenecks in Roc because tasks and closures are unboxed and resolved via enumerated direct calls rather than dynamic dispatch. - [13:20](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=800s) Immutability becomes a major performance win in UI rendering because pointer equality checks (oldState === newState) skip costly virtual DOM diffing and re-renders entirely. - [15:51](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=951s) Traditional pure functional implementations of mutation-heavy algorithms like Quicksort face severe allocation penalties unless they resort to unsafe imperative escape hatches. - [21:14](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1274s) Opportunistic in-place mutation allows a compiler to safely mutate memory buffers directly whenever it proves the target data structure is never referenced again in the program. - [23:58](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1438s) While dynamic runtime reference counting can trigger in-place mutation when a ref count equals one, compile-time alias analysis achieves the same result with zero runtime branch overhead. - [23:58](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1438s) Roc integrates the Morphic solver, an open-source Rust alias analysis library developed at UC Berkeley, to detect uniqueness and mutation opportunities at compile time. - [28:43](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1723s) In a 10-million-number sorting benchmark, Roc's purely functional Quicksort outperformed imperative implementations in JavaScript, Java, Go, and mutable Haskell, trailing only C++. - [32:32](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1952s) Pure functional languages can outperform imperative equivalents via three distinct mechanisms: problem-space alignment (like fast UI equality), low baseline language overhead, or aggressive compiler optimizations.
## Detailed walkthrough ### [00:06](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=6s) Introducing Roc: Bringing the Joy of Elm to Native Binaries
Richard Feldman opens by introducing his background and motivation: for the past couple of years, he has been building Roc, a new pure functional programming language. Roc is directly inspired by and descended from Elm, which has been Feldman’s favorite language for several years. Elm delivers a remarkably delightful user experience that makes programming a genuine joy, but because it compiles to JavaScript, it is primarily tailored for browser-based user interfaces. Feldman wanted that same delightful pure functional developer experience in broader domains outside the browser. Roc adopts Elm’s design philosophy and purity, but compiles directly to standalone machine binaries to target entirely different use cases.
Feldman recalls that his initial entry point into Elm was an influential 2014 blog post titled *Blazing Fast HTML in Elm*. At the time, he was building a performance-sensitive UI, and seeing Elm's benchmarks gave him confidence that he would not have to sacrifice speed for a pleasant developer experience. He carried this commitment into Roc’s design, emphasizing: “it's really important to me that like hey let's go fast let's let's build a programming language that runs programs really fast.”
However, aiming for high performance in a pure functional language poses a major conceptual challenge: pure functional programming strictly prohibits side effects and in-place mutation. Because certain classic algorithms fundamentally rely on mutation to achieve optimal speed, the paradigm seems inherently constrained:
> “The fact of the matter is some algorithms just require in-place mutation to run fast.”
Feldman highlights quicksort as an obvious example—raising the question of how competitive a purely functional language could ever hope to be at such an inherently mutation-heavy task. To evaluate whether pure functional languages can genuinely rival imperative ones, Feldman proposes looking across multiple performance dimensions, beginning with the baseline language overhead and how close a language's runtime model can get to the raw hardware.
---
[02:08](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=128s) Language Overhead: Lessons from Haskell at NoRedInk
To illustrate how language overhead can outweigh paradigm differences, Feldman shares real-world production experience from his company, NoRedInk. For years, NoRedInk ran its backend primarily on Ruby on Rails. Recently, the team has been migrating core backend components to Haskell, the most widely known pure functional programming language. In particular, they rewrote their highest-traffic, most performance-critical service in Haskell. This service powers software for English teachers and processes billions of student-answered questions each year.
When the service was migrated from Ruby on Rails to Haskell, the team observed a massive performance boost:
> “The delta was quite large in terms of performance it was about 10x faster.”
Feldman is careful to clarify that this 10x gain was not due to some magical speed inherent to pure functional code. Rather, it came down to execution models and runtime overhead. Haskell's compiled, static nature simply outpaced Ruby's interpreted execution, demonstrating that underlying language mechanics often dominate the theoretical trade-offs between imperative and purely functional styles.
```mermaid flowchart LR accTitle: Execution Models from Highest to Lowest Overhead accDescr: A pipeline showing interpreted ASTs down to LLVM-optimized binaries.
subgraph Interpreted ["Interpreted"] A["Interpreted AST<br/>(Python, Ruby, JS)"] B["Bytecode Interpreters<br/>(Java, C#, Elixir/BEAM)"] end
subgraph Compiled ["Compiled to Native"] C["Direct to Binary<br/>(Go, Swift, Haskell)"] D["LLVM-Optimized Binary<br/>(C++, Rust, Zig, Roc)"] end
A --> B B --> C C --> D ```
Feldman breaks language execution models down from highest to lowest overhead: - **Interpreted ASTs**: Languages like Python, Ruby, and JavaScript parse source text into an abstract syntax tree and interpret it on the fly, occasionally using Just-In-Time (JIT) compilation to accelerate hot loops. - **Interpreted Bytecode / VMs**: Platforms like the JVM (Java), CLR (C#), and BEAM (Elixir/Erlang) compile source into bytecode executed by a virtual machine. - **Direct Standalone Binaries**: Languages such as Go, Swift, and Haskell compile directly to native machine binaries, stripping away interpreter overhead. - **LLVM-Optimized Binaries**: Languages like C++, Rust, Zig, and Roc compile through LLVM. Feldman highlights LLVM as the gold standard library for aggressive binary optimization to extract the best possible hardware performance.
Beyond compilation, Feldman identifies **boxing** as another major source of overhead. Storing values as heap-allocated pointers rather than placing them directly on the call stack or into CPU registers incurs serious costs. In languages like Python, even basic numbers are boxed, necessitating third-party native libraries like NumPy to restore mathematical performance. Depending on the language, objects, records, functions, and closures are also frequently boxed, dragging down execution speed.
---
[05:04](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=304s) Memory Management Strategies and Roc's Approach
Feldman reviews the primary memory management paradigms available to modern languages, contrasting their trade-offs in throughput and latency:
| Strategy | Languages | Strengths | Drawbacks | |:--- |:--- |:--- |:--- | | **Manual / Individual Allocation** | C, C++, Rust, Zig | Fine-grained control; no GC pauses; high performance | Historically required tedious manual `malloc`/`free` calls (though modern languages automate lifecycles) | | **Tracing Garbage Collection** | Go, JavaScript, Haskell, Java | Developer convenience; prevents memory leaks | GC pauses where the runtime freezes execution to sweep unreachable memory | | **Automatic Reference Counting (ARC)** | Python, Swift, Roc | Immediate reclamation; avoids stop-the-world GC pauses | Higher runtime overhead per operation; lower overall throughput than tracing GC |
To eliminate ARC's typical throughput penalties without reintroducing garbage collection pauses, Roc uses **static reference counting**. Drawing from two academic papers—*Counting Immutable Beans* and *Perseus*—Roc analyzes references at compile time. If the compiler proves that increments and decrements cancel each other out, it simply eliminates those operations entirely from the generated machine code.
Feldman notes an additional novel memory management feature in Roc: in certain situations, Roc can completely transparently swap out reference counting for **arena allocation** (bump allocation). Feldman previously delivered a dedicated talk on this topic, observing that arena allocation runs faster than any other strategy—including tracing GCs and manual heap allocations. While not the focus of this talk, it represents Roc's long-term strategy for high-throughput memory management.
In summarizing Roc’s foundational language design, Feldman concludes:
> “When it comes to language overhead we've basically tried to do as good of a job when we can of minimizing the language overhead of Roc.”
To achieve this, Roc combines: - Native binary compilation via **LLVM** - Fully **unboxed values** wherever possible (eliminating unnecessary heap-pointer indirection) - **Static reference counting** to prune runtime ref-count operations, backed by transparent arena allocation
Having covered how Roc tackles language-level overhead, Feldman prepares to confront the harder half of the equation: the performance impact of pure immutability and zero side effects.
[08:03](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=483s) Side Effects vs. Managed Effects
When examining pure functional programming, developers must confront two foundational constraints: the prohibition of side effects and the prohibition of mutation. The restriction against side effects often sounds far more crippling to newcomers than it actually is in practice. A frequent and skeptical reaction among skeptics is to ask whether pure functional programs are simply uselessly churning the processor without accomplishing real work. As Richard Feldman explains, this misunderstanding ignores reality:
> “Pascal doesn't do side effects like how is most of our production traffic running on haskell is it just like not doing any effects is it never talking to database no of course not it's doing all those effects they're just not side effects still effects but they are now managed effects.”
The real performance trade-off is not between doing work and doing nothing, but between side effects and managed effects. In an imperative ecosystem like JavaScript, asynchronous tasks are frequently modeled with Promises. Code might invoke `promise1 = fetch(url)` and chain operations via `promise2 = promise1.then(callback)`. In pure functional languages like Elm, managed effects look almost identical structurally—using an Elm pipeline like `|> Task.andThen` instead of `.then()`. The crucial mechanical distinction lies in execution timing: when an imperative Promise is instantiated, it triggers the side effect immediately (for example, the HTTP request fires the instant `fetch()` is called). In contrast, managed effects merely assemble an inert, chained description of a task. That data structure is passed off to `main`, and the language runtime executes the actual operations when the program runs. The trade-off is that while imperative code can intersperse normal function calls alongside Promises, a pure functional language must run *every* effect through this wrapped, task-based abstraction.
```mermaid flowchart TD accTitle: Imperative Promises vs Managed Effects accDescr: Comparison showing that JavaScript Promises execute immediately upon instantiation whereas Managed Effects build a pipeline that the runtime runs later.
subgraph Imperative ["Imperative (JavaScript Promises)"] A["Call fetch(url)"] -->|"Instantiates promise"| B["Execute HTTP Request Immediately"] B --> C["Chain with.then(callback)"] end
subgraph PureFunctional ["Pure Functional (Elm / Roc Tasks)"] D["Define Task Pipeline"] -->|"Chain with andThen"| E["Build Unexecuted Task Structure"] E -->|"Pass structure to main"| F["Runtime Executes Effect on Launch"] end ```
[10:21](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=621s) The Overhead of Closures and Unboxed Tasks in Roc
Because every single effect in a pure functional language must reside inside a wrapper like a Task, the critical performance question becomes the cost of that wrapper. Both Promises and Tasks are fundamentally closures—functions that capture their enclosing lexical scope—wrapping other closures. Therefore, the architectural overhead of managed effects is directly equivalent to the cost of closures in a given language runtime. In many systems, closures are boxed (allocated on the heap), which incurs pointer indirection and GC pressure, significantly slowing down program execution.
In the Roc programming language, this overhead is minimized at the compiler level. Roc does not box functions or closures; it keeps tasks unboxed and applies defunctionalization, turning closure calls into enumerated direct calls rather than invoking dynamic function pointers. As Feldman observes regarding whether wrapping every effect degrades system throughput:
> “all of which basically means that the overhead of this these like wrapped effects that we have to do everywhere it's so minimal that it's honestly not something we're even worried about”
- Managed tasks and their nested closures in Roc remain entirely unboxed on the heap. - Defunctionalization replaces indirect function pointers with fast, enumerated direct calls. - The theoretical penalty of wrapping every effect is negligible enough to be dismissed as a performance bottleneck.
[11:48](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=708s) The Cost of Immutability vs. The In-Place Mutation Advantage
While managing effects introduces minimal friction, the absence of mutation presents a far more severe performance obstacle. When optimizing low-level algorithms, the prohibition against in-place mutation is a substantial disadvantage. Feldman points out the gravity of this restriction:
> “this is sort of the big one like this is this is like the big downside of pure functional programming um is that um that this makes performance optimization in some scenarios a lot harder”
A simple comparison between an imperative `for` loop and a functional `array.map` illustrates the divergence. In JavaScript, an imperative loop (`for (let i = 0; i < array.length; i++)`) can directly double every element in place (`array[i] *= 2`). Because it mutates the existing buffer, it avoids new allocations, maximizes CPU cache locality, and runs fast. The pure functional equivalent (`array.map(x => x * 2)`) cannot modify the buffer; it must allocate an entirely new array and write doubled values into those newly claimed memory locations, which may not reside in the cache. When raw performance demands in-place updates, copying memory cannot directly compete with direct in-place mutation.
[13:00](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=780s) When Immutability Wins: Fast Equality Checks in Rendering
Despite the inherent allocation penalty of immutability, there are scenarios where forcing immutability produces a net performance win. Feldman refers back to the frontend rendering benchmarks that initially drew him to Elm, where pure functional and immutable implementations systematically outpaced traditional imperative alternatives. In virtual DOM and UI rendering pipelines, verifying whether a UI subtree needs to re-render requires comparing the previous state with the incoming state.
Because immutability guarantees that an object’s contents never change, the runtime can perform an instantaneous pointer equality check (`oldState === newState`). If the pointers match, the entire rendering subtree can be skipped without inspecting nested fields. With in-place mutation, the previous state is overwritten, making fast reference comparison impossible and forcing costly deep object traversals. Feldman articulates the trade-off that makes Elm and similar tools fast:
> “so basically when the cost of immutability which is a cost in this case um is less than the sort of like cost of rendering it can actually end up working out to be a net performance improvement uh which is exactly how these libraries and languages are able to win at this benchmark”
- Immutability introduces an upfront allocation penalty, but allows structural checks via single-instruction pointer equality. - The technique was first popularized for UI rendering performance by David Nolen in ClojureScript. - Evan Czaplicki, creator of Elm, subsequently built on Nolen's observation to achieve top-tier speeds in browser rendering benchmarks.
[14:43](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=883s) Mutation-Heavy Algorithms and the Quicksort Challenge
The rendering optimization is an architectural win where the cost of rendering exceeds the cost of copying. However, many classical computer science workloads cannot trade memory allocation for algorithmic shortcuts. When tackling mutation-heavy algorithms, in-place updates are mandatory for optimal execution speed. To explore whether a pure functional language can ever truly compete with imperative languages on these workloads, Feldman selects quicksort as a representative benchmark proxy.
Quicksort relies heavily on partitioning: it selects pivots, executes comparisons, and performs in-place element swaps within a contiguous array buffer. While comparisons can be modeled purely, the swap step (`swap(array, i, j)`) fundamentally requires writing directly into array slots. Looking at a standard, non-hand-optimized JavaScript quicksort implementation reveals an inherently imperative structure: an outer `for` loop, mutable index increments, and repeated in-place swaps. If pure functional languages are forced to allocate a fresh array on every swap, they risk lagging far behind imperative baselines.
[15:51](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=951s) Haskell and the Quicksort Dilemma
Richard Feldman begins by addressing a classic computer science problem: implementing quicksort directly out of a textbook without custom micro-optimizations. While pure functional languages like Haskell have low baseline overhead compared to languages like Ruby—allowing them to excel in many real-world workloads—quicksort presents a notorious conceptual roadblock. Haskell is famous for a textbook one-liner quicksort that is clean, elegant, and concise, but it faces a valid criticism: “the whole point of quicksort is that it's fast because of in-place mutation this is just doing a bunch of cloning it's not fast it's not quick so why are we considering this to be quicksort”.
To explore that critique, Feldman examines an alternative Haskell implementation highlighted in a community response where the author accepted the challenge to write an in-place version. This version is far less concise, but its real drawback is behavioral rather than aesthetic. In the code, function calls like `unsafeRead`, `unsafeSwap`, and `unsafeSlice` from `Data.Vector.Generic.Mutable` pop up. The code drops the pretense of purely functional programming and relies on raw imperative mutation, hoping nobody notices. The documentation for `unsafeSlice` specifically warns that it yields part of a mutable vector without copying and without performing bounds checks. This forfeits referential transparency completely, leaving the code less safe than Rust and squarely in C and C++ territory, where an out-of-bounds read leads directly to segmentation faults, garbage memory, and security vulnerabilities. Feldman remarks: “this function's all kinds of unsafe so it's a good thing they put unsafe in the title i appreciate that”.
```mermaid flowchart TD accTitle: Pure Functional Quicksort vs Imperative In-Place Mutation accDescr: Diagram showing how elegant functional quicksort relies on cloning, while fast in-place mutation requires bypassing safety invariants.
subgraph Functional["Pure Haskell Approach"] A["Concise recursive definition"] --> B["Heap allocations & cloning"] B --> C["Safe, referentially transparent, but slow"] end
subgraph Unsafe["Imperative Haskell Approach"] D["Data.Vector.Generic.Mutable"] --> E["unsafeRead / unsafeSwap / unsafeSlice"] E --> F["No bounds checks & direct memory mutation"] F --> G["Fast in-place sorting, but risks segfaults"] end ```
[18:11](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1091s) Benchmarking Quicksort Across Languages
Curious about how this "cheating" imperative Haskell holds up under load, Feldman ran an experiment quicksorting 10 million numbers across multiple languages, executing the sorting routine 100 times per language on the same machine to average the results. Even though it violated functional paradigms, Haskell finished slightly ahead of JavaScript (which came in at just under 1.4 seconds), while Java outperformed Haskell, Go outperformed Java, and C++ came out on top as the fastest. Feldman cautions the audience against making broad proclamations about one language being universally faster than another from this benchmark, emphasizing: “can't emphasize this enough this is not a rigorous comparison this is really just to talk about you know performance optimization and pure functional languages”.
The benchmark revealed that minor idiomatic adjustments significantly shift language overhead without changing the fundamental algorithmic complexity: - **Java**: Switching from `ArrayList` to a standard primitive `int[]` array yielded a massive speedup, which was kept because both represent reasonable standard implementations. - **JavaScript**: Sorting 64-bit integers initially triggered severe performance hits despite native `BigInt` support; switching the data type to 64-bit floating point numbers (`double`), JavaScript's native sweet spot, dramatically leveled the playing field. - **C++**: Idiomatic C++ skips bounds checks by default, conferring a distinct speed advantage over memory-safe runtimes. - **Alternative escape hatches**: Feldman notes that in real production systems, Haskell programmers seeking speed typically turn to foreign function interface (FFI) interoperability with low-level systems languages like C, C++, Rust, or Zig—the same strategy Python uses with NumPy—rather than littering their codebases with internal `unsafe` mutations.
[21:14](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1274s) Compiler Optimization and Opportunistic In-Place Mutation
Feldman pivots from manual workarounds and FFI calls to an alternative question: can an optimizing compiler bridge the performance gap automatically? Historically, compilers have routinely surprised skeptics; the earliest optimizing Fortran compilers generated machine code that outpaced hand-written assembly, establishing a precedent that human coders might eventually be unable to beat automated optimizers. In pure functional programming, Tail Call Optimization (TCO) serves as the primary proof-of-concept for this philosophy. Mathematically, any imperative loop construct (`for`, `while`) can be expressed as a recursive function. Left alone, naive recursion imposes distinct call-stack overhead, but an optimizing compiler detecting tail calls can emit the exact same assembly jump instructions that an imperative loop would produce.
Extending that insight to memory mutations, Feldman poses the core dilemma: can a purely functional transformation like `Array.map` compile down to the exact machine instructions of an in-place imperative `for` loop? The immediate roadblock is that true in-place modification destroys immutability. However, an optimizer can exploit a critical loophole: “if we have to happen to be in the situation where the thing that we're mapping over is never referenced again for the rest of the program there's no downside it's never referenced again yeah we're mutating in place but no one ever sees that”. If the runtime can guarantee that the original reference is dead, modifying the memory buffer directly preserves pure referential transparency to the outside world while matching the raw performance of imperative mutation. This compiler-driven optimization is known as opportunistic in-place mutation.
[23:58](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1438s) Opportunistic In-Place Mutation and Static Analysis
When an operation appears to require an immutable clone semantically, a compiler can optimize it into an in-place mutation if it is completely unobservable from the perspective of the program. Richard Feldman explains that this can be done automatically through runtime reference counting, which is what the Roc programming language uses: if a data structure is about to be updated and its reference count is exactly one, no other part of the program holds a reference to it. In that scenario, the runtime can safely mutate the memory in place rather than allocating a fresh copy. However, this dynamic approach requires reference counting rather than a tracing garbage collector, because a tracing GC does not maintain precise reference counts for individual values.
```mermaid flowchart TD accTitle: In-place mutation detection strategies accDescr: Flowchart contrasting dynamic reference count checks at runtime with static alias analysis at compile time.
subgraph DynamicCheck["Dynamic Approach (Runtime)"] RC["Inspect Reference Count"] RC -->|"Count == 1"| MutateDirect["Mutate In-Place"] RC -->|"Count > 1"| AllocClone["Clone to New Buffer"] end
subgraph StaticCheck["Static Approach (Compile Time)"] AliasAnalysis["Morphic Solver / Alias Analysis"] AliasAnalysis -->|"Guaranteed unique"| DirectInPlace["Compile as Direct Mutation"] AliasAnalysis -->|"Potential sharing"| FallbackRC["Fall back to Runtime RC Check"] end ```
A vast amount of the time, this check does not even need to occur at runtime. A compiler can determine statically that a reference count will be one because the data structure is simply never accessed again in the remainder of the program. Feldman underscores the power of this static optimization: “So this is basically a way to detect many but not quite all but many very many of the these in-place mutation optimization opportunities at compile time.” Static detection eliminates the runtime overhead of branch comparisons, works across all of Roc's memory management backends, and applies equally well to unboxed values like records, which are not reference-counted at all.
- **Folkert de Vries's Thesis:** The initial prototype in Roc was built by Folkert de Vries for his master's thesis at Radboud University using uniqueness types inspired by the Clean programming language; however, it proved brittle because small, unrelated code changes could easily de-optimize a function. - **The Morphic Solver:** Roc migrated to a second-generation approach based on alias analysis powered by the Morphic solver, an open-source Rust library built by undergraduate researchers at UC Berkeley. - **Roc Collaboration:** Feldman jokes about his own undergraduate days (“I played video games in undergrad”), praising the Berkeley team for building a pluggable static analysis library that directly helps Roc reach its aggressive performance goals.
---
[26:35](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1595s) Pure Functional Quicksort in Roc
To put these compiler techniques to the test, Feldman examines Quicksort, an algorithm that is traditionally considered a worst-case scenario for pure functional programming. In an imperative language like JavaScript, Quicksort relies heavily on mutating arrays in place, updating pointers, and executing a tight `for` loop. Roc implements Quicksort without any imperative escape hatches: loop iteration is replaced with a tail-recursive helper function (`partitionHelp`), and in-place element swapping is expressed as an immutable function that semantically returns a brand-new list alongside updated index records.
``` +-----------------------------------------------------------------------------------------+ | QUICKSORT COMPARISON | +-----------------------------------------------------------------------------------------+ | Feature | Imperative (e.g., JavaScript) | Pure Functional (Roc) | +----------------------+------------------------------------+-----------------------------+ | Loop Mechanism | Imperative `for` loop | Tail-recursive helper calls | | Element Swapping | Direct in-place array swap | Semantically returns record | | Algorithmic State | Mutation of index variables (i++) | Returns new index records | | Surface Code Feel | Concise, native imperative idiom | Pathological, verbose code | | Executed Machine Ops | Mutates backing array directly | Mutates buffer via Morphic | +----------------------+------------------------------------+-----------------------------+ ```
Looking at the side-by-side comparison, Feldman readily admits that the functional implementation looks far more verbose and appears to do vastly more work on the surface: “Now this is actually pretty much a pathological algorithm for pure functional programming like it requires in-place mutation to be fast and that's just like not something that pure FP excels at.” While Haskell had previously posted respectable quicksort numbers in benchmarks by resorting to imperative features under the hood, Roc's version remains strictly pure. When compiled, the static alias analysis combined with LLVM lowering transforms all the record allocations and list copies into the exact same low-level in-place swaps that an imperative algorithm would execute.
---
[28:43](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1723s) Benchmark Results and the Anatomy of Roc's Speed
When the benchmark results are displayed, Roc's purely functional Quicksort outperforms almost the entire field of imperative competitors. Roc finishes faster than JavaScript, Java, Go, and even the "cheating" imperative Haskell implementation, landing second only to C++. Feldman recounts the team's reaction to the breakthrough: “Not gonna lie we just got all of this working like two days ago and like within an hour or two we were like okay well like how do we close that gap with C++?” Because both languages share the same asymptotic complexity, stripping away unnecessary language overhead allowed Roc to pull ahead of heavily optimized garbage-collected runtimes.
```mermaid flowchart LR accTitle: Optimization layers enabling Roc's performance accDescr: Diagram illustrating the five compiler and language layers that give Roc high performance.
LLVM["LLVM Backend<br/>(C++/Rust-grade passes)"] Unbox["Unboxed Values<br/>(Zero heap allocations)"] RC["Static RC<br/>(Deterministic cleanup)"] TCO["Tail-Call Optimization<br/>(No stack growth)"] InPlace["Opportunistic Mutation<br/>(Morphic alias analysis)"]
LLVM --- Unbox --- RC --- TCO --- InPlace ```
Feldman breaks down the specific technical pillars that make this speed possible: - **LLVM Optimizer:** Powers Roc's code generation, giving it the exact same machine-code optimization passes used by C++, Rust, and Zig. - **Unboxed Values:** Keeps primitive types and records on the stack or in registers, preventing heap allocation overhead. - **Static Reference Counting:** Employs deterministic memory reclamation without pausing for a tracing garbage collector. - **Tail Call Optimization (TCO):** Crucial for recursive algorithms like Quicksort; without TCO, Feldman notes that “you just sit and watch the computer do nothing for a while and then eventually stack overflow.” - **Opportunistic In-Place Mutation:** Feldman emphasizes this breakthrough, noting: “And then finally the real key that you know without which this is just totally hopeless is the opportunistic in-place mutation.” While academic research languages have experimented with this, Roc aims to bring it into an industry-ready production language.
---
[30:52](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1852s) Language Goals: Elm's Heritage and Overhead Reduction
Feldman steps back to place these benchmark results into the broader vision for Roc. Roc is designed as a direct descendant of Elm, seeking to capture the delightful developer experience, robust ergonomics, and purity of Elm while escaping the confines of browser-based user interfaces. Elm proved how productive and maintainable pure functional programming could be on the web, and Roc seeks to bring that exact paradigm to compiled standalone binaries that can compete directly with modern systems languages.
Achieving native performance requires navigating three distinct engineering challenges: general language runtime overhead, the absence of side effects, and the lack of native mutation. Many developers mistakenly blame the functional paradigm itself when a functional codebase runs slowly, failing to realize that general runtime overhead—such as memory layout, boxing, and runtime indirection—is often the real bottleneck. When a pure functional compiler addresses those mechanical overheads directly and turns pure semantics into in-place machine operations, functional code can match or exceed traditional imperative software.
[31:47](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1907s) Reducing Language Overhead and Evaluating Roc's Architecture
Reflecting on real-world engineering choices, Richard Feldman highlights the ongoing backend transition at NoRedInk from Ruby to Haskell. As the company migrated the bulk of its web traffic over to Haskell services, they observed a dramatic 10x improvement in performance. Taking advantage of the moment, Feldman slips in a recruiting pitch for engineers keen on Elm, Haskell, and potentially Roc down the road. This transition illustrates a broader design goal: minimizing baseline language overhead as much as possible. In Roc, this philosophy is pushed further by targeting LLVM as the compilation backend, keeping values unboxed across the board, and adopting lightweight memory management techniques like static reference counting alongside other novel mechanisms discussed in Feldman's earlier talks.
At the same time, Feldman offers an honest caveat about the maturity of Roc's implementation. Because Roc remains an active work in progress without massive production codebases deployed in the wild, the team cannot yet verify with absolute certainty how static reference counting will behave at massive scale. Despite the absence of giant Roc applications, early benchmarks and structural experiments leave the team feeling optimistic about their current design path.
- NoRedInk observed an approximate 10x performance gain after moving the majority of its production web traffic from Ruby to Haskell. - Roc keeps runtime overhead minimal by pairing an LLVM backend with aggressive unboxing and static reference counting. - “We don't actually know to be fair like how how well the static reference counting is going to work at scale because Roc is a very work in progress language it's like not really being used for anything serious yet there are no sufficiently big Roc programs to say that but so far we're feeling pretty good about about the direction we're going,” Feldman acknowledges.
[32:32](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=1952s) Managed Effects, Immutability, and Outperforming Imperative Languages
Turning to runtime behavior, Feldman contrasts uncontrolled side effects with managed effects. Roc disallows arbitrary side effects, requiring developers to describe operations through managed effects—a model that feels functionally similar to working with Promises in JavaScript, with the caveat that tasks or promises must be used universally for all I/O. The critical factor is whether that abstraction introduces unacceptable execution costs. By relying on unboxed tasks, unboxed closures, and enumerated direct calls instead of indirect dynamic dispatch, Roc eliminates this penalty so thoroughly that managed effects cease to be a meaningful source of overhead.
```mermaid flowchart TD accTitle: Sources of Pure Functional Performance accDescr: Three ways pure functional programming matches or outperforms imperative code: managed effects, structural immutability, and compiler optimizations.
subgraph Effects["Managed Effects Pipeline"] E1["Unboxed Tasks"] --> E2["Unboxed Closures"] E2 --> E3["Enumerated Direct Calls"] end
subgraph Performance["Why FP Outperforms Imperative"] P1["Inherent Paradigm Fit<br/>(e.g., Virtual DOM skipping in browser)"] P2["Low Baseline Overhead<br/>(e.g., Haskell beating Ruby)"] P3["Compiler Optimizations<br/>(e.g., In-place mutation for Quicksort)"] end
E3 --> P3 ```
Feldman then addresses immutability. Although generating fresh values rather than modifying them in place is traditionally regarded as a runtime cost, it can serve as a potent optimization in environments like browser rendering, where persistent data structures allow libraries to instantly detect changes and bypass unnecessary re-computation. When algorithms demand in-place mutation to stay competitive—such as Quicksort, which is historically pathological for purely functional paradigms—Roc leverages its suite of compiler optimizations to update data structures in place behind a pure interface. The result is pure functional code that not only avoids running slower than imperative equivalents, but can even outperform them.
- Managed effects in Roc are implemented using unboxed tasks, unboxed closures, and enumerated direct calls, preventing them from becoming a performance bottleneck. - Immutability can act as an optimization when UI libraries exploit reference equality to skip rendering and tree-diffing work. - Aggressive optimizations allow purely functional implementations of Quicksort to execute in-place mutations under the hood and beat imperative counterparts. - Performance advantages arise from three distinct avenues: inherent alignment with problem domains (e.g., UI rendering), radically lower baseline language overhead (e.g., compiled FP vs. dynamic imperative), or sheer compiler optimization effort. - “And hopefully now you have an idea of how you actually can outperform imperative with pure functional languages thanks very much,” Feldman concludes.
## Notable quotes > [00:06](https://www.youtube.com/watch?v=vzfy4EKwG_Y&t=6s) The fact of the matter is some algorithms just require in-place mutation to run fast. — Richard Feldman
## Mentioned Richard Feldman, Roc, Elm, Haskell, NoRedInk, Ruby on Rails, JavaScript, Python, Java, Go, C++, Rust, Zig, Swift, LLVM, NumPy, Counting Immutable Beans, Perseus, David Nolen, Evan Czaplicki, ClojureScript, Folkert de Vries, Clean, Morphic, UC Berkeley, Radboud University
## Video description Like many, I once thought that functional programming makes programs easier to maintain, but slower to run. After all, that had been my experience; when I wrote JavaScript in a functional style, my code got easier to maintain - but it no longer ran as fast.
Once I got deeper into functional languages, including pure functional ones, I was surprised to discover that they could actually outperform imperative languages - not just in tasks generally considered "well-suited" to functional programming, but sometimes even in cases considered well-suited for imperative programming!
This talk examines performance from the network level all the way down to individual CPU instructions to show how and when pure functional programming languages can outperform their imperative counterparts. Come see just how fast functional programming can be!
Richard Feldman NoRedInk @rtfeldman
Richard Feldman is a software engineer at NoRedInk and a well-known member of the Elm community.
Recorded at Strange Loop 2021 https://thestrangeloop.com
[Source](https://www.youtube.com/watch?v=vzfy4EKwG_Y)