<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Serokell Blog</title>
        <link>https://serokell.co/blog</link>
        <description>Your expectations, lifted.</description>
        <lastBuildDate>Wed, 10 Jun 2026 16:16:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Serokell Blog</title>
            <url>https://serokell.co/preview/default.png</url>
            <link>https://serokell.co/blog</link>
        </image>
        <copyright>2026 Serokell</copyright>
        <atom:link href="https://serokell.co/blog.rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Rust, C++, and the Tradeoffs Behind Safe Low-Level Code: interview with Nikita Lisitsa]]></title>
            <link>https://serokell.co/blog/rust-c-and-the-tradeoffs-behind-safe-low-level-code-interview-with-nikita-lisitsa</link>
            <guid isPermaLink="false">https://serokell.co/blog/rust-c-and-the-tradeoffs-behind-safe-low-level-code-interview-with-nikita-lisitsa</guid>
            <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, we interview Nikita Lisitsa about C++, Rust, systems programming, and game development.
We discuss whether C++ is still the default path into systems programming, where Rust fits in, and how both languages may coexist over the next decade. Nikita also shares his perspective on memory safety, language complexity, game engine design, real-time physics simulations, renderer abstractions, and the lessons he learned from shipping Costa Verde.]]></description>
            <content:encoded><![CDATA[<p>In this post, we interview Nikita Lisitsa about C++, Rust, systems programming, and game development.</p>
<p>We discuss whether C++ is still the default path into systems programming, where Rust fits in, and how both languages may coexist over the next decade. Nikita also shares his perspective on memory safety, language complexity, game engine design, real-time physics simulations, renderer abstractions, and the lessons he learned from shipping Costa Verde.</p>
<p><img src="https://serokell.co/files/a6/a6y6ars.personal_card-Vitaly_Bragilevsky_(2).jpg" alt="a6y6ars.personal_card-Vitaly_Bragilevsky_(2).jpg"></p>
<h2 id="rust-advertises-its-%E2%80%9Cif-it-compiles-%E2%80%93-it-works%E2%80%9D-stance-and-its-user-friendly%2Feducational-compiler-messages.-should-we-start-learning-rust-as-a-first-step-to-c%2B%2B%3F" tabindex="-1"><strong>Rust advertises its “if it compiles – it works” stance and its user-friendly/educational compiler messages. Should we start learning Rust as a first step to C++?</strong></h2>
<p>I’d say both languages should be learnt in parallel. Both Rust and C++ have a ton of quirks and idiosyncrasies specific to these languages (borrow checker in Rust, duck-typed templates in C++, etc), and I don’t think they are good prerequisites to each other. However, they share a lot, too (being low-level, RAII/Drop trait, caring about object ownership &amp; lifetimes), which is why I think it’s a good idea to learn both at the same time.</p>
<h2 id="rust%E2%80%99s-compiler-and-safety-guarantees-allow-%E2%80%9Clearning-by-trial-and-error%E2%80%9D-in-multi-threaded-synchronization.-should-we-use-rust-as-training-wheels-while-learning-parallel-programming%3F" tabindex="-1"><strong>Rust’s compiler and safety guarantees allow “learning by trial and error” in multi-threaded synchronization. Should we use Rust as training wheels while learning parallel programming?</strong></h2>
<p>I don’t have a strong opinion on this. Rust forces you to wrap shared data in Arc&lt;Mutex&lt;T&gt;&gt; or something like that, which does prevent data races, but also hides a lot of underlying complexity, and doesn’t prevent a lot of other problems like deadlocks. C++ has a lot of other stuff you have to care about when writing multi-threaded code. To be honest, something like Java still feels like the cleanest language to <em>learn</em> the basics of parallel programming, and then maybe C++ to <em>learn</em> the more complicated, low-level things. But for <em>writing</em> multi-threaded code, Rust is probably the best.</p>
<h2 id="generalising-those-two-questions-%E2%80%93-is-c%2B%2B-still-the-best-language-to-get-into-system-programming%3F" tabindex="-1"><strong>Generalising those two questions – is C++ still the best language to get into system programming?</strong></h2>
<p>I don’t think there is a “best” language for system programming, it really depends on what your goals are. Writing Linux drivers and writing high-load multi-threaded data-processing servers are very different tasks. C, C++, Rust, Go, and a ton of other languages can be great for system programming in a suitable context. C++ is generally a pretty good match, but sometimes the manual memory management and unexpected UB can make it not worth it.</p>
<h2 id="c%2B%2B-keeps-accumulating-features-%E2%80%94-modules%2C-coroutines%2C-reflection%2C-contracts.-do-you-think-the-language-is-becoming-too-complex-to-use-safely-and-teachably%2C-or-is-that-complexity-justified%3F" tabindex="-1"><strong>C++ keeps accumulating features — modules, coroutines, reflection, contracts. Do you think the language is becoming too complex to use safely and teachably, or is that complexity justified?</strong></h2>
<p>The way I see it, there is a certain programming language spectrum between simplicity and complexity. It’s basically a balance of how much you have to keep in your head vs how much your language does for you, but you still have to keep in your head that the language does it for you. C, for example, is closer to the “simplicity” end of this spectrum, while C++ has always been on the opposite end. I personally love C++ for that: you have to learn a lot about what the language can do, but then it pays off as you use what you’ve learnt to your advantage to write simpler, more expressive code.</p>
<p>The current development of C++ follows that same pattern: more complexity that empowers the programmer. There have always been people who refuse to use most of C++ features, and only use some subset of the language, and it makes sense that from their perspective, the language is becoming worse. I personally think it is justified in the context of using C++ in its fullness.</p>
<h2 id="do-you-think-c%2B%2B-is-a-good-choice-for-agentic-development%3F" tabindex="-1"><strong>Do you think C++ is a good choice for agentic development?</strong></h2>
<p>Never tried agentic development, so I can’t tell :)</p>
<h2 id="with-all-this-ai-stuff-going-on%2C-shouldn%E2%80%99t-memory-safety%2Fcorrectness-be-the-first-priority-of-language-design%3F" tabindex="-1"><strong>With all this AI stuff going on, shouldn’t memory safety/correctness be the first priority of language design?</strong></h2>
<p>Memory correctness is just one type of bugs, and, contrary to the widespread misconception, it isn’t the most common bug we have in C++ or any other language. C++, for instance, has been evolving to make memory safety almost effortless — things like smart pointers and proper usage of RAII cover 95% of all such cases. The tooling has been evolving, too — ASAN, for instance, can find most memory-related bugs automatically. Typically, our bugs are simply errors in the program’s logic, and there’s little you can do in the language itself to prevent those. I’d say the focus should be on something like contract-based programming (or, better, proof-based languages that use dependent types / HoTT / etc), and not some specific type of common bugs like invalid memory access.</p>
<h2 id="in-game-development%2Fsystem-programming%2C-it-is-considered-a-good-practice-to-avoid-allocations-at-all-costs.-did-rust-people-get-it-all-wrong-with-all-the-fuzz-around-ownership%3F" tabindex="-1"><strong>In game development/system programming, it is considered a good practice to avoid allocations at all costs. Did Rust people get it all wrong with all the fuzz around ownership?</strong></h2>
<p>The way I see it, allocations and ownership semantics are largely orthogonal. You can use stack variables, a dedicated frame allocator, or make an entirely custom global allocator both in C++ and in Rust.</p>
<p>That being said, I personally believe that the “avoid allocations” mantra is a bit of a cargo cult, and probably stems from the early 2000s, when OOP was the cool way to write code, and everyone allocated everything on the heap for no reason at all, leading to degraded performance. The right thing to do is always to analyze what’s the real performance drain. In my code, I often allocate tons of temporary arrays or strings (e.g. for UI), and it turns out that this is pretty much never the performance bottleneck unless you heap-allocate individual tiny objects or something like that.</p>
<h2 id="c%2B%2B-is-still-the-language-of-choice-for-game-engine-development.-what-does-rust-miss%2C-and-will-it-ever-be-able-to-compete-with-c%2B%2B%3F" tabindex="-1"><strong>C++ is still the language of choice for game engine development. What does Rust miss, and will it ever be able to compete with C++?</strong></h2>
<p>I don’t think Rust misses anything in particular. It can be hard to write complicated data processing code with nontrivial inter-object dependencies and a ton of mutable state (which is what game logic often looks like) in Rust, but it should be manageable once you do it a few times and settle on some fitting patterns and data structures. Also, game development often requires a ton of experimenting and ad-hoc tweaking, and Rust’s strictness introduces extra friction.</p>
<p>I believe the reason most game engines still use C++ is mostly inertia. I don’t just mean that people simply don’t want a new language when the old one does the job (though this is a reason as well), but also that C++ has several decades of surrounding tooling, libraries, and platform support (afaik various game console SDKs only support C++).</p>
<h2 id="your-engine-seems-to-provide-low-level-graphics%2Fplatform%2Faudio%2Futility-infrastructure%2C-while-you-often-reimplement-the-project-specific-renderer-per-game.-where-do-you-draw-the-boundary-between-%E2%80%9Cengine-feature%E2%80%9D-and-%E2%80%9Cgame-specific-system%2C%E2%80%9D-and-can-you-give-an-example-of-a-system-you-first-put-into-the-engine-but-later-decided-should-live-in-the-game%3F" tabindex="-1"><strong>Your engine seems to provide low-level graphics/platform/audio/utility infrastructure, while you often reimplement the project-specific renderer per game. Where do you draw the boundary between “engine feature” and “game-specific system,” and can you give an example of a system you first put into the engine but later decided should live in the game?</strong></h2>
<p>I usually draw the boundary using two criteria: how good the code quality is, and how useful and generic this system is. Ultimately, it is based on how I feel about the code. There were instances of the code migrating into the engine and back. For example, there is a fairly generic deferred renderer in the engine, but I’ve never used it in any project, as I love playing with various styles and techniques and making the rendering engine from scratch is just easier. There is a 2d physics engine as well, which I never used outside of a few toy demonstrations — again, because it’s easier to make a dedicated simple physics engine tailored to a specific project.</p>
<p>On the other hand, the engine contains a very primitive 2D painter class that can draw lines, circles, sprites, and text. While the code quality and the API of this class are rather questionable, I’ve used it numerous times because it’s basically all you need for a simple 2D game on a game jam, which is why it’s part of the base engine as well.</p>
<h2 id="in-your-soft-body-spaceship-work-and-water-over-terrain-simulation%2C-you-repeatedly-choose-simplified-models-that-preserve-the-gameplay-relevant-behavior-while-avoiding-full-physical-realism.-how-do-you-decide-which-invariants-must-be-preserved%2C-such-as-stability%2C-mass-conservation%2C-energy-behavior%2C-or-locality%2C-and-which-physical-inaccuracies-are-acceptable-for-a-game%3F" tabindex="-1"><strong>In your soft-body spaceship work and water-over-terrain simulation, you repeatedly choose simplified models that preserve the gameplay-relevant behavior while avoiding full physical realism. How do you decide which invariants must be preserved, such as stability, mass conservation, energy behavior, or locality, and which physical inaccuracies are acceptable for a game?</strong></h2>
<p>It mostly boils down to how the physics simulation interacts with gameplay, and how costly the simulation is. In my soft-body game for Ludum Dare 53, you play as a soft squishy spaceship. Hare, stability is essential, as we don’t want the spaceship to explode, while conservation of momentum and angular momentum aren’t that important. In fact, I introduce artificial friction (which, of course, doesn’t exist in actual space) to dampen momentum over time, to help stabilize the ship if it’s rotating wildly, to prevent the player from accidentally flying too far away, and to force the player to think strategically about fuel consumption when planning distant trips. The simulation itself is extremely cheap, by the way, so performance isn’t an issue here.</p>
<p>Water simulation is way more complicated. Typical methods involve iteratively solving large sparse linear systems, with many iterations per frame. Together with my goal of simulating a reasonably large portion of terrain with a somewhat high resolution (say, 200x200 meters with 1 meter resolution), it’s not really feasible to do that in real-time. (I could do this on the GPU, but it’s already busy doing the rendering, and it only makes things faster by some constant factor.) So, I have to drastically simplify the model in order to achieve real-time performance. There are some properties of the model that I can’t sacrifice — for example, local mass conservation is crucial to prevent small puddles or lakes from disappearing (some models I’ve tried before had this problem). Energy conservation isn’t that important, though, — as with the squishy spaceship, I’ll introduce some friction anyway to make the water surface calm down over time. The method I’ve chosen (called <em>virtual pipes</em>) has a ton of drawbacks — for example, it lacks inertia entirely, — but it fits the other criteria, and is as fast as it could be.</p>
<h2 id="you-have-used-opengl-3.3%2C-ported-toward-webgpu%2C-built-webgpu-demos%2Fraytracing%2C-and-used-webgpu-compute-for-browser-simulations.-if-you-were-designing-your-renderer-backend-abstraction-today%2C-what-would-you-expose-as-stable-engine-level-concepts%2C-and-what-would-you-avoid-abstracting-because-webgpu%2C-opengl%2C-and-future-apis-differ-too-much%3F" tabindex="-1"><strong>You have used OpenGL 3.3, ported toward WebGPU, built WebGPU demos/raytracing, and used WebGPU compute for browser simulations. If you were designing your renderer backend abstraction today, what would you expose as stable engine-level concepts, and what would you avoid abstracting because WebGPU, OpenGL, and future APIs differ too much?</strong></h2>
<p>I actually don’t like the idea of a “renderer backend” for my personal projects. I usually rewrite the rendering engine from scratch for each project, using some rendering API like OpenGL or WebGPU directly. There are some things that seem common across many rendering engines, like textures or 3D models, but I find that it’s hard to make them truly generic because each project has its special needs. Maybe you need instancing, maybe you need some extra vertex or instance attributes to control per-vertex wind strength, maybe you need to pass an extra per-instance color transformation matrix, etc. If you try to accommodate all possible use cases, you end up with something that basically directly mimics the underlying API with no real benefit other than satisfying one’s need for abstraction. So, in some sense, I avoid abstracting the API altogether. This means that switching APIs for a particular project can be hard, but that doesn’t happen too often. In fact, it never happened before, but I might rewrite my current project in Vulkan. The rendering code is about 10k lines, and most of them aren’t related to the underlying API at all, so I don’t expect it to be that complicated.</p>
<h2 id="costa-verde-was-your-first-commercial-release%2C-and-your-projects-show-many-jam-games-and-technical-prototypes.-looking-back%2C-which-technical-bets-paid-off-when-shipping-costa-verde%2C-and-which-engine-or-design-choices-slowed-you-down-the-most%3F-how-would-those-lessons-change-your-current-village-building-game%E2%80%99s-architecture%2C-production-scope%2C-and-tool-priorities%3F" tabindex="-1"><strong>Costa Verde was your first commercial release, and your projects show many jam games and technical prototypes. Looking back, which technical bets paid off when shipping Costa Verde, and which engine or design choices slowed you down the most? How would those lessons change your current village-building game’s architecture, production scope, and tool priorities?</strong></h2>
<p>This might sound surprising, but, for the most part, releasing Costa Verde didn’t really teach me anything about the technical side of game development that I didn’t already know. There weren’t really any technical bets either: I used well-supported technologies (OpenGL 3.3, SDL2, etc) which are known to work, and, well, they did work perfectly. The game isn’t really that rendering-intensive, so I didn’t need a sophisticated graphics API.</p>
<p>I did learn one architectural thing, though. The game features a lot of clearly-defined object classes, like road segments, cars, buildings, traffic lights (those were removed in the final game, though), etc, and they all were mostly just stored in big arrays. I had to support arbitrary removal from these arrays, so I had to use some generation-based handles to reference the objects instead of bare indices or pointers. If you look closely, this is basically half of the implementation of a typical ECS engine (the other half being the components — adding/removing/querying/etc). Realizing that I’ve made several independent half-baked ECS implementations throughout the game’s code convinced me to implement a proper full ECS engine for the next project.</p>
<h2 id="the-3-year-release-cycle-%E2%80%94-is-it-the-right-cadence%3F-too-fast%2C-too-slow%2C-or-does-the-cadence-even-matter-given-how-long-adoption-takes%3F" tabindex="-1"><strong>The 3-year release cycle — is it the right cadence? Too fast, too slow, or does the cadence even matter given how long adoption takes?</strong></h2>
<p>I think it’s alright. The adoption of new standards is rather slow, but I’d guess if the standards were released faster, the adoption would be faster as well, and the same if it were slower. The programming language landscape is extremely diverse these days, and languages are forced to evolve as fast as they can. However, releasing too often leads to a versioning hell. So, to me the 3-year cycle feels about right.</p>
<h2 id="how-do-you-see-c%2B%2B-and-rust-coexisting-over-the-next-decade%3F-competition%2C-gradual-replacement-in-some-domains%2C-or-peaceful-coexistence%3F" tabindex="-1"><strong>How do you see C++ and Rust coexisting over the next decade? Competition, gradual replacement in some domains, or peaceful coexistence?</strong></h2>
<p>To me, it feels that it will be a peaceful competition, with varying rates of success of both languages in different fields. I don’t think Rust will be able to replace C++ entirely, but it definitely will succeed in some areas, especially where safety is a high concern. I’d say that Rust has a higher chance of replacing C, though there are other contenders for that as well (e.g. Zig or Odin).</p>
<h2 id="the-us-government-and-nsa-have-recommended-moving-away-from-c%2Fc%2B%2B-toward-memory-safe-languages.-does-that-concern-you%2C-and-how-do-you-think-the-c%2B%2B-community-should-respond%3F" tabindex="-1"><strong>The US government and NSA have recommended moving away from C/C++ toward memory-safe languages. Does that concern you, and how do you think the C++ community should respond?</strong></h2>
<p>I’m all for using Rust in safety-critical software, but I don’t think this recommendation makes sense for software in general. As I’ve said before, I believe that, in general everyday-use software, memory bugs are a minor problem, and by that logic we should all move to programming in proof assistants instead.</p>
<p>That being said, currently there are many proposals to make C++ itself safer, and while this hasn’t lead to any particular large language feature like a borrow checker, it’s still heading in an interesting direction.</p>
<hr>
<p>Blog: <a href="https://lisyarus.github.io/blog/about.html">https://lisyarus.github.io/blog/about.html</a><br>
X: <a href="https://x.com/lisyarus">https://x.com/lisyarus</a><br>
YouTube: <a href="https://www.youtube.com/@lisyarus">https://www.youtube.com/@lisyarus</a><br>
StackOverflow: <a href="https://stackoverflow.com/users/2315602/lisyarus">https://stackoverflow.com/users/2315602/lisyarus</a></p>
]]></content:encoded>
            <author>hi+briankean@serokell.co (Brian Kean)</author>
            <enclosure url="https://serokell.co/files/aw/thumb.awn7jvx.lisitsabig.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Serokell’s Work on GHC: Dependent Types, Part 5]]></title>
            <link>https://serokell.co/blog/serokell-s-work-on-ghc-dependent-types-part-5</link>
            <guid isPermaLink="false">https://serokell.co/blog/serokell-s-work-on-ghc-dependent-types-part-5</guid>
            <pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This article continues the fine tradition of Serokell's GHC team sharing their progress on bringing dependent types to Haskell. A lot has happened since the last report, and there is plenty to cover.

In this edition, Vladislav Zavialov presents three major contributions and a host of smaller improvements that push Dependent Haskell closer to becoming a practical reality.]]></description>
            <content:encoded><![CDATA[<p>This article continues the fine tradition of Serokell’s GHC team sharing their progress on bringing dependent types to Haskell. A lot has happened since the last report, and there is plenty to cover.</p>
<p>In this edition, Vladislav Zavialov presents three major contributions and a host of smaller improvements that push Dependent Haskell closer to becoming a practical reality.</p>
<h2 id="summary" tabindex="-1">Summary</h2>
<p>The highlights of this report are:</p>
<ul>
<li>Visible <code>forall</code> in GADTs</li>
<li>Namespace-specified imports</li>
<li>Type instances in kind checking</li>
</ul>
<p>After that, we are going to go through a number of other improvements:</p>
<ul>
<li>Progress on unifying <code>HsType</code> and <code>HsExpr</code></li>
<li>The star kind syntax in required type arguments</li>
<li>Pun detection in required type arguments</li>
<li>New type families: <code>Tuple</code>, <code>Constraints</code>, <code>Tuple#</code>, <code>Sum#</code></li>
<li>Rework of name resolution for built-in and punned names</li>
</ul>
<h2 id="visible-forall-in-gadts" tabindex="-1">Visible <code>forall</code> in GADTs</h2>
<p>The design of dependent types for Haskell, as described by <a href="https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0378-dependent-type-design.rst">GHC Proposal #378</a> “Design for Dependent Types”, includes support for at least 6 quantifiers:</p>
<pre><code class="hljs">Quantifier        Dependence     <span class="hljs-keyword">Visibility</span>     Erasure
------------------------------------------------------------
<span class="hljs-keyword">forall</span> a. ty      <span class="hljs-keyword">Dependent</span>      Invisible      Erased
<span class="hljs-keyword">forall</span> a -&gt; ty    <span class="hljs-keyword">Dependent</span>      Visible        Erased
foreach a. ty     <span class="hljs-keyword">Dependent</span>      Invisible      Retained
foreach a -&gt; ty   <span class="hljs-keyword">Dependent</span>      Visible        Retained
Eq a =&gt; ty        Non-<span class="hljs-built_in">dependent</span>  Invisible      Retained
t1 -&gt; t2          Non-<span class="hljs-built_in">dependent</span>  Visible        Retained
</code></pre>
<p>The one we all care about is <code>foreach a -&gt; ty</code>, also known as the dependent product, dependent function, or Π-type (all three are synonymous). But before we can tackle something so ambitious, it helps to deal with the other quantifiers, such as <code>forall a -&gt; ty</code>, often referred to as the visible <code>forall</code> or VDQ (visible dependent quantification).</p>
<p>The majority of design questions for VDQ were resolved when <a href="https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0281-visible-forall.rst">GHC Proposal #281</a> “Visible forall in types of terms” was accepted back in 2021, and we have been relentlessly chipping away at its implementation ever since, one engineering challenge at a time.</p>
<p>The latest advancement in this direction is the implementation of VDQ in GADTs. Starting with GHC 9.14, the <code>RequiredTypeArguments</code> extension allows declarations such as the following:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">data</span> <span class="hljs-type">T</span> a where</span>
  <span class="hljs-type">Typed</span> :: <span class="hljs-keyword">forall</span> a -&gt; a -&gt; <span class="hljs-type">T</span> a
</code></pre>
<p>In this example, the <code>Typed</code> data constructor takes two visible arguments: a <em>type</em>, and then a <em>value</em> of that type, e.g. <code>Typed Int 42</code> or <code>Typed String &quot;hello&quot;</code>. This surely has a dependently-typed look to it! Here are some examples of what it might look like in various contexts:</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- Expressions</span>
<span class="hljs-title">t1</span> = <span class="hljs-type">Typed</span> <span class="hljs-type">Int</span> <span class="hljs-number">42</span>
<span class="hljs-title">t2</span> = <span class="hljs-type">Typed</span> <span class="hljs-type">String</span> <span class="hljs-string">&quot;hello&quot;</span>
<span class="hljs-title">t3</span> = <span class="hljs-type">Typed</span> (<span class="hljs-type">Int</span> -&gt; <span class="hljs-type">Bool</span>) even

<span class="hljs-comment">-- Patterns</span>
<span class="hljs-title">f1</span> (<span class="hljs-type">Typed</span> a x) = x :: a
<span class="hljs-title">f2</span> (<span class="hljs-type">Typed</span> <span class="hljs-type">Int</span> n) = n*<span class="hljs-number">2</span>
<span class="hljs-title">f3</span> (<span class="hljs-type">Typed</span> ((-&gt;) w <span class="hljs-type">Bool</span>) g) = not . g

<span class="hljs-comment">-- Types (with DataKinds)</span>
<span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-type">T1</span> = <span class="hljs-type">Typed</span> <span class="hljs-type">Nat</span> 42</span>
<span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-type">T2</span> = <span class="hljs-type">Typed</span> <span class="hljs-type">Symbol</span> &quot;hello&quot;</span>
<span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-type">T3</span> = <span class="hljs-type">Typed</span> (<span class="hljs-type">Type</span> -&gt; <span class="hljs-type">Constraint</span>) <span class="hljs-type">Num</span></span>
</code></pre>
<p>One thing to keep in mind, and why this doesn’t really count as dependent types, is that the type argument is guaranteed to be erased, i.e. has no effect on how data is represented on the heap, and consequently can’t be pattern matched on:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">f4</span> (<span class="hljs-type">Typed</span> a x) =
  <span class="hljs-keyword">case</span> a <span class="hljs-keyword">of</span>   <span class="hljs-comment">-- Nuh-uh! This is a compile-time error</span>
    <span class="hljs-type">Int</span>  -&gt; negate x
    <span class="hljs-type">Bool</span> -&gt; not x
    _    -&gt; x
</code></pre>
<p>Nonetheless, allowing this form of quantification comes with its own technical challenges, which are now behind us. This means that when it comes to adding proper dependent types, we won’t have to worry about syntactic trivia.</p>
<p>Here is what it took to make this work.</p>
<p>The first challenge was that GHC’s AST for constructor patterns <code>MkE @tp1 @tp2 p1 p2</code> kept the type arguments and term arguments in entirely separate lists. We refactored it to use a mixed-list representation:</p>
<pre><code class="hljs language-diff">ConPat &quot;MkE&quot; [tp1, tp2] [p1, p2]               -- old
ConPat &quot;MkE&quot; [InvisP tp1, InvisP tp2, p1, p2]  -- new
</code></pre>
<p>The immediate effect of the new representation was an improvement to error messages. Consider the pattern <code>Con x @t y</code>. Previously it resulted in a parse error because <code>@t</code> could not occur after <code>x</code>, and now it is reported as <code>[GHC-14964]</code>.</p>
<p>More interestingly, the form <code>Con x @t y</code> has become, in principle, representable, so it has become possible to handle it properly down the compilation pipeline.</p>
<p>The second challenge was to allow visible <code>forall</code> in constructor signatures. It took a few tries to get this right, as the forall-or-nothing rule that governs implicit quantification meant that some quantifiers had to be kept in a separate field. The type checker was also updated to no longer assume that all subpatterns without the <code>@</code> herald were value arguments. Indeed, some or all of them may very well turn out to be required type arguments.</p>
<p>The third challenge was to update the Core representation of data constructors to allow foralls of varying visibility to occur in the list of quantifiers:</p>
<pre><code class="hljs language-diff"><span class="hljs-deletion">- dcUserTyVarBinders :: [InvisTVBinder]</span>
<span class="hljs-addition">+ dcUserTyVarBinders :: [TyVarBinder]</span>
</code></pre>
<p>This change not only necessitated mechanical changes scattered throughout the GHC codebase, but also resulted in cryptic Core Lint errors. The patch had been shelved for a few months until <code>@mniip</code> helped to debug this issue at ZuriHac 2025. As it turned out, the predicate that determines whether to introduce a data constructor wrapper was returning a false negative.</p>
<p>With all three challenges resolved, GHC can now handle the following example (constructed to stress-test the feature rather than demonstrate a realistic use case):</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Checker</span> a b c where</span>
  <span class="hljs-type">C</span> :: <span class="hljs-keyword">forall</span> a. <span class="hljs-keyword">forall</span> b -&gt; <span class="hljs-keyword">forall</span> c. (a -&gt; (c, b)) -&gt; <span class="hljs-type">Checker</span> a b c

<span class="hljs-title">cDouble</span> = <span class="hljs-type">C</span> <span class="hljs-type">Double</span> (length &amp;&amp;&amp; read)

<span class="hljs-comment">-- ghci&gt; map (test cDouble) [&quot;1.5&quot;, &quot;0.5&quot;, &quot;1.05&quot;, &quot;0.05&quot;]</span>
<span class="hljs-comment">-- [(3,True),(3,True),(4,True),(4,False)]</span>
<span class="hljs-title">test</span> :: <span class="hljs-type">Show</span> b =&gt; <span class="hljs-type">Checker</span> <span class="hljs-type">String</span> b c -&gt; <span class="hljs-type">String</span> -&gt; (c, <span class="hljs-type">Bool</span>)
<span class="hljs-title">test</span> (<span class="hljs-type">C</span> t f) s = fmap (\r -&gt; show @t r == s) (f s)
</code></pre>
<p>And there we have it: we are one step closer to freely mixing terms and types in our Haskell programs. Future work in that direction includes nested quantification in GADTs (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/18389">#18389</a>) and VDQ in pattern synonyms (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/23704">#23704</a>).</p>
<h2 id="namespace-specified-imports" tabindex="-1">Namespace-specified imports</h2>
<p>Haskell has two namespaces, as <a href="https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0581-namespace-specified-imports.rst">GHC Proposal #581</a> explains:</p>
<ul>
<li>the <em>type namespace</em>, including the names of type constructors, type synonyms, type families and type classes;</li>
<li>the <em>data namespace</em>, including the names of term-level values, functions, data constructors and pattern synonyms.</li>
</ul>
<p>This separation allows us to define data constructors and type constructors whose names coincide:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">data</span> <span class="hljs-type">T</span> = <span class="hljs-type">T</span></span>
</code></pre>
<p>At use sites, GHC infers which <code>T</code> is referred to from the context. For example, in <code>t :: T</code> the occurrence of <code>T</code> resolves to the type constructor, whereas in <code>t = T</code> to the data constructor.</p>
<p>The reliance on context to select the namespace creates ambiguities when it comes to import/export lists, fixity declarations, pragmas, TH name quotation, and most notably when mixing terms and types with <code>DataKinds</code> and <code>RequiredTypeArguments</code>.</p>
<p>The easiest way to sidestep these issues is to avoid introducing type and data constructor names that coincide, and this is what dependent-types-flavored Haskell tends to do. However, we can’t expect all libraries to adopt such a convention, so we still need ways to disambiguate <code>T</code> against <code>T</code> in a context-independent manner.</p>
<p>After a number of false starts, with no fewer than three proposals from various authors, the consensus was to introduce namespace-specified imports looking like this:</p>
<pre><code class="hljs language-haskell"><span class="hljs-keyword">import</span> Data.Monoid <span class="hljs-keyword">as</span> M.Type (<span class="hljs-title">type</span> ..)
<span class="hljs-keyword">import</span> Data.Monoid <span class="hljs-keyword">as</span> M (<span class="hljs-title">data</span> ..)
</code></pre>
<p>Notice the new <code>..</code> syntax; it is a wildcard that stands for all names in the specified namespace.</p>
<p>Recall that the <code>Data.Monoid</code> module exports the following newtypes:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">All</span>       = <span class="hljs-type">All</span>     {<span class="hljs-title">getAll</span> :: <span class="hljs-type">Bool</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Alt</span> f a   = <span class="hljs-type">Alt</span>     {<span class="hljs-title">getAlt</span> :: <span class="hljs-title">f</span> <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Any</span>       = <span class="hljs-type">Any</span>     {<span class="hljs-title">getAny</span> :: <span class="hljs-type">Bool</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Ap</span> f a    = <span class="hljs-type">Ap</span>      {<span class="hljs-title">getAp</span> :: <span class="hljs-title">f</span> <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Dual</span> a    = <span class="hljs-type">Dual</span>    {<span class="hljs-title">getDual</span> :: <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Endo</span> a    = <span class="hljs-type">Endo</span>    {<span class="hljs-title">appEndo</span> :: <span class="hljs-title">a</span> -&gt; <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">First</span> a   = <span class="hljs-type">First</span>   {<span class="hljs-title">getFirst</span> :: <span class="hljs-type">Maybe</span> <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Last</span> a    = <span class="hljs-type">Last</span>    {<span class="hljs-title">getLast</span> :: <span class="hljs-type">Maybe</span> <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Product</span> a = <span class="hljs-type">Product</span> {<span class="hljs-title">getProduct</span> :: <span class="hljs-title">a</span>}</span>
<span class="hljs-class"><span class="hljs-keyword">newtype</span> <span class="hljs-type">Sum</span> a     = <span class="hljs-type">Sum</span>     {<span class="hljs-title">getSum</span> :: <span class="hljs-title">a</span>}</span>
</code></pre>
<p>With imports written as above, one can refer to the type constructors as <code>M.Type.Dual</code>, <code>M.Type.Endo</code>, <code>M.Type.Product</code>, and so on, and to the data constructors as <code>M.Dual</code>, <code>M.Endo</code>, and <code>M.Product</code> respectively.</p>
<p>At the outset, this seemed like a straightforward feature to implement: a new form of import/export item that simply selects names according to the namespace specifiers. However, as we started to tackle this problem, we found a surprising number of pre-existing bugs that had to be fixed first:</p>
<ul>
<li>In subordinate import and export lists, the use of the <code>type</code> namespace specifier used to be silently ignored (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/12488">#12488</a>, <a href="https://gitlab.haskell.org/ghc/ghc/-/issues/22581">#22581</a>)</li>
<li>In subordinate import lists within a <code>hiding</code> clause, non-existent items led to a poor warning message with <code>-Wdodgy-imports</code> (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25983">#25983</a>)</li>
<li>In subordinate import lists within a <code>hiding</code> clause, non-existent items resulted in the entire import declaration being discarded (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25984">#25984</a>)</li>
<li>In subordinate import lists, it was not possible to refer to a class
method if there was an associated type of the same name (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25991">#25991</a>)</li>
</ul>
<p>With so many corner cases discovered, it was no longer clear how many other bugs were lurking in the import/export logic, but we decided to proceed cautiously with preparations:</p>
<ul>
<li>
<p>We introduced the option to use the <code>data</code> keyword with individual import/export items, e.g. <code>import Data.Proxy as D (data Proxy)</code>.</p>
</li>
<li>
<p>In accordance with the proposal, we deprecated the <code>pattern</code> namespace specifier and introduced the <code>-Wpattern-namespace-specifier</code> warning to aid migration to the new <code>data</code> syntax.</p>
</li>
<li>
<p>We increased the test coverage of <code>-Wduplicate-exports</code> and <code>-Wdodgy-exports</code>, which revealed some typos and dead code.</p>
</li>
</ul>
<p>At last, it seemed like the foundation was solid enough to tackle the actual feature. The next step was to add support for top-level namespace-specified wildcards <code>type ..</code> and <code>data ..</code> to import and export lists:</p>
<pre><code class="hljs language-haskell"><span class="hljs-keyword">import</span> M (<span class="hljs-title">type</span> ..) <span class="hljs-comment">-- imports all type and class constructors from M</span>
<span class="hljs-keyword">import</span> M (<span class="hljs-title">data</span> ..) <span class="hljs-comment">-- imports all data constructors and terms from M</span>
</code></pre>
<pre><code class="hljs language-haskell"><span class="hljs-keyword">module</span> M (<span class="hljs-title">type</span> .., <span class="hljs-title">f</span>) <span class="hljs-keyword">where</span>
  <span class="hljs-comment">-- exports all type and class constructors defined in M,</span>
  <span class="hljs-comment">-- plus the function &#x27;f&#x27;</span>
</code></pre>
<p>We then carried out a refactoring, after which the implementation resumed and reached another milestone: support for subordinate namespace-specified wildcards <code>X(type ..)</code> and <code>X(data ..)</code> in import and export lists:</p>
<pre><code class="hljs language-haskell"><span class="hljs-keyword">import</span> M (<span class="hljs-type">Cls</span>(<span class="hljs-title">type</span> ..))  <span class="hljs-comment">-- imports Cls and all its associated types</span>
<span class="hljs-keyword">import</span> M (<span class="hljs-type">Cls</span>(<span class="hljs-title">data</span> ..))  <span class="hljs-comment">-- imports Cls and all its methods</span>
</code></pre>
<pre><code class="hljs language-haskell"><span class="hljs-keyword">module</span> M (<span class="hljs-type">R</span>(<span class="hljs-title">data</span> ..), C(<span class="hljs-title">type</span> ..)) <span class="hljs-keyword">where</span>
  <span class="hljs-comment">-- exports R and all its data constructors and record fields;</span>
  <span class="hljs-comment">-- exports C and all its associated types, but not its methods</span>
</code></pre>
<p>At this point, GHC could handle all examples from the proposal, so we might as well declare victory. Studying the spec revealed a few corner cases that are still being worked on (<a href="https://gitlab.haskell.org/ghc/ghc/-/issues/27268">#27268</a>), but those are unlikely to arise in practice.</p>
<p>Et voilà, Haskellers have another tool to deal with the Dreaded Namespace Problem.</p>
<h2 id="type-instances-in-kind-checking" tabindex="-1">Type instances in kind checking</h2>
<p>If you ever wrote <code>$(return [])</code> in your module to help GHC kind-check your code, you know what this section is going to be about. If not, read on: this one is a heavy hitter.</p>
<p>We are excited to announce that, after 10 years of futile attempts, we finally taught GHC to find open type family instances during kind checking, regardless of the order in which they are written in the source file. Consider the following program:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-keyword">family</span> <span class="hljs-type">Open</span> a</span>
<span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-keyword">family</span> <span class="hljs-type">F</span> a :: <span class="hljs-type">Open</span> a</span>

<span class="hljs-class"><span class="hljs-keyword">type</span> instance <span class="hljs-type">F</span> <span class="hljs-type">Int</span> = <span class="hljs-type">True</span></span>
<span class="hljs-class"><span class="hljs-keyword">type</span> instance <span class="hljs-type">Open</span> <span class="hljs-type">Int</span> = <span class="hljs-type">Bool</span></span>
</code></pre>
<p>When kind-checking the <code>F Int = True</code> instance, we need to know that <code>Open Int = Bool</code>, so we’d better check the other type instance first, otherwise we’ll see the following error:</p>
<pre><code class="hljs"><span class="hljs-keyword">error: </span>[GHC<span class="hljs-string">-83865</span>]
    • Expected kind ‘Open Int’, but ‘True’ has kind ‘Bool’
    • In the type ‘True’
      In the type instance declaration for ‘F’
</code></pre>
<p>But look again at this line:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">type</span> instance <span class="hljs-type">F</span> <span class="hljs-type">Int</span> = <span class="hljs-type">True</span></span>
</code></pre>
<p>It mentions <code>F</code>, <code>Int</code>, and <code>True</code>. Nothing points to <code>Open</code>! So how can we know to kind-check the other instance first?</p>
<p>We tried various heuristics, but every time, we found corner cases that couldn’t be handled. In the meantime, some variation of this issue was reported at the rate of about once a year:</p>
<ul>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/12088">#12088</a> “Type/data family instances in kind checking” (May 20, 2016)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/12239">#12239</a> “Dependent type family does not reduce” (June 29, 2016)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/13790">#13790</a> “GHC doesn’t reduce type family in kind signature unless its arm is twisted” (June 5, 2017)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/14668">#14668</a> “Ordering of declarations can cause typechecking to fail” (January 14, 2018)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/15561">#15561</a> “Type error conditioned on ordering of GADT and type family definitions” (August 24, 2018)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/16410">#16410</a> “Order of declarations matters” (March 8, 2019)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/16448">#16448</a> “Unresolved type family kind bug” (March 16, 2019)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/16693">#16693</a> “Order of declarations affects which programs are accepted” (May 25, 2019)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/19611">#19611</a> “Typechecking of dependently-kinded type family instances doesn’t have enough equalities in scope” (March 28, 2021)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/20875">#20875</a> “Declaration order of aliases and type families” (December 26, 2021)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/21172">#21172</a> “No reduction of kind families” (March 5, 2022)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/22257">#22257</a> “Dependent type families cannot be separated from their instances” (October 5, 2022)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25238">#25238</a> “Kind reduction with open type families depends on data type declaration order” (September 5, 2024)</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25834">#25834</a> “Splices seem to break type inference for type families” (March 8, 2025)</li>
</ul>
<p>Haskellers are phenomenal at writing programs that break the compiler, and thanks to the bug reports, we have accumulated an excellent set of test cases.</p>
<p>Let’s now dive into the technical details.</p>
<p>After renaming type, class, and instance declarations in a module, GHC does dependency analysis on the renamed declarations to figure out in which order to kind-check them. The dependency analysis returns a topologically sorted list of SCCs (strongly-connected components), where an SCC represents a set of mutually recursive declarations.</p>
<p>If the dependency analysis were complete, i.e. if it were able to discover all dependencies between all declarations, then GHC could simply kind-check SCCs in order. Unfortunately, this is not the case, because dependencies come in two varieties:</p>
<ul>
<li>
<p>Lexical dependencies arise when <code>X</code> mentions <code>Y</code> by name:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">data</span> <span class="hljs-type">X</span> (<span class="hljs-title">a</span> :: <span class="hljs-type">Y</span>) = <span class="hljs-type">MkX</span>   <span class="hljs-comment">-- depends on Y</span></span>
<span class="hljs-class"><span class="hljs-keyword">data</span> <span class="hljs-type">Y</span> = <span class="hljs-type">MkY</span></span>
</code></pre>
</li>
<li>
<p>Non-lexical dependencies arise when an instance must be in the typing environment:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-keyword">family</span> <span class="hljs-type">F</span> x</span>
<span class="hljs-class"><span class="hljs-keyword">data</span> <span class="hljs-type">X</span> (<span class="hljs-title">a</span> :: <span class="hljs-type">F</span> <span class="hljs-type">Int</span>) = <span class="hljs-type">MkX</span> a   <span class="hljs-comment">-- depends on (F Int ~ Type)</span></span>
<span class="hljs-class"><span class="hljs-keyword">type</span> instance <span class="hljs-type">F</span> x = <span class="hljs-type">Type</span></span>
</code></pre>
</li>
</ul>
<p>Non-lexical dependencies can’t be discovered by looking at the free variables of a declaration, and attempts to find a good heuristic did not bear fruit. As a consequence, the order of SCCs coming out of the renamer is determined solely by lexical dependencies.</p>
<p>In other words, the SCCs are arranged in <em>lexical dependency order</em>, meaning:</p>
<ul>
<li>definitely in dependency order if all dependencies are lexical</li>
<li>possibly not in dependency order if there are non-lexical dependencies</li>
</ul>
<p>Here is another example of how type checking declarations might go wrong due to non-lexical dependencies:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-keyword">family</span> <span class="hljs-type">F</span> a</span>
<span class="hljs-class"><span class="hljs-keyword">type</span> instance <span class="hljs-type">F</span> <span class="hljs-type">Int</span> = <span class="hljs-type">Bool</span></span>

<span class="hljs-class"><span class="hljs-keyword">data</span> <span class="hljs-type">R</span> = <span class="hljs-type">MkR</span> (<span class="hljs-type">F</span> <span class="hljs-type">Int</span>)</span>

<span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-type">S</span> = <span class="hljs-type">MkR</span> <span class="hljs-type">True</span></span>
</code></pre>
<p>For <code>S</code> to kind-check, we need to know that <code>(F Int) ~ Bool</code>. But we won’t know that unless we’ve looked at the type instance declaration for <code>F</code> before kind-checking <code>S</code>.</p>
<p>The solution we ended up adopting is to discover the correct kind-checking order by trial and error. The algorithm works as follows:</p>
<ol>
<li>
<p>Perform the dependency analysis on declarations without instances and considering
lexical dependencies only. The result is a topologically sorted list of SCCs.</p>
</li>
<li>
<p>Create one singleton “SCC” per instance and put them at the end.</p>
</li>
<li>
<p>Check all SCCs in order, skipping any that are blocked (free variables not in the environment), flawed (unusable unpack pragmas), or failing (type errors)</p>
<ul>
<li>(3a) if none were skipped, we are done</li>
<li>(3b) if all were skipped and none are flawed, we are stuck; report errors and exit</li>
<li>(3c) if all were skipped and some are flawed, redo the pass allowing flawed SCCs</li>
<li>(3d) if some were skipped and some weren’t, we’ve made progress; iterate</li>
</ul>
</li>
</ol>
<p>In the common case of lexical dependencies only, the algorithm is linear in the number of groups: it completes in one pass if the program is kind-correct, or two passes if there are kind errors.</p>
<p>In the less common case of non-lexical dependencies, the algorithm is worst-case quadratic in the number of groups: if each pass manages to check only one group, we end up doing a pass per group.</p>
<p>Now, regarding the “flawed” groups. These are the ones where the programmer used the <code>{-# UNPACK #-}</code> pragma on a field, yet we could not unpack. One possible reason for this is that we lack a data instance in the environment that would allow for the field to be unpacked, so it is beneficial to treat this like a kind error: skip the flawed group and retry it in a later pass, when we might have more data instances in the environment. However, if the <em>only</em> reason a pass gets stuck is due to flawed groups, then we can make progress by treating unpacking failure as a warning. This way, we maximize unpacking with explicit <code>{-# UNPACK #-}</code> pragmas. Later we might check SCCs for other “flaws”, but for now the property is just about unusable unpack pragmas.</p>
<p>Finally, a short comment on why it is necessary to check whether a group is ready (all free variables are in the environment) or blocked (some free variables are not in the environment). One might expect this check to be redundant, as the SCCs come in lexical dependency order. However, as soon as we skip a group, the rest of the pass can no longer rely on this property, hence the check. It is rare to encounter this problem in a kind-correct program, but we managed to construct a test case.</p>
<p>That is all for the technical details. It took many iterations to arrive at this algorithm. Each previous attempt failed on some corner case or another, which is, of course, what made this a 10-year problem to begin with. If you are one of the many Haskellers who ran into this bug and found yourself reordering declarations or reaching for the <code>$(return [])</code> workaround, you can now drop it. GHC 9.14 handles it automatically.</p>
<p>Beyond the immediate quality-of-life improvement, this fix unblocks exciting developments such as singletonisation of GADTs.</p>
<h2 id="progress-on-unifying-hstype-and-hsexpr" tabindex="-1">Progress on unifying <code>HsType</code> and <code>HsExpr</code></h2>
<p>Internally, GHC’s frontend uses two distinct types to represent types and terms: <code>HsType</code> and <code>HsExpr</code> respectively. A dependently typed language would normally give uniform treatment to types and terms, so one of our goals is a refactoring to use one representation for both, arriving at the following declaration in GHC:</p>
<pre><code class="hljs language-haskell"><span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-type">HsType</span> = <span class="hljs-type">HsExpr</span></span>
</code></pre>
<p>The rationale for this is to increase code reuse between the term- and type-level pipelines in the compiler front-end (AST, parser, renamer, type checker).</p>
<p>Given that types and terms already share a number of similarities, one would expect this to be a straightforward refactoring. For example, the following constructs are found in both:</p>
<ul>
<li>Variables <code>a</code></li>
<li>Constructors <code>Con</code></li>
<li>Prefix applications <code>f a</code></li>
<li>Infix applications <code>a # b</code></li>
<li>Type applications <code>f @t</code></li>
<li>Literals <code>42</code>, <code>&quot;hello&quot;</code></li>
<li>Signatures <code>a :: t</code></li>
<li>Lists <code>[a, b, c]</code></li>
<li>Tuples <code>(a, b, c)</code></li>
<li>Parentheses <code>(a)</code></li>
<li>Holes <code>_</code></li>
</ul>
<p>However, as it turned out, the representations for those constructs in <code>HsExpr</code> and <code>HsType</code> differ in various ways, leading to what one might call “death by a thousand cuts”.</p>
<p>We expect that it will take a good number of patches to handle each discrepancy individually and work out its user-facing consequences. For example, when we dealt with a difference in how infix operator applications were represented, we ended up allowing infix holes <code>a`_`b</code> in types, following the precedent set by term-level expressions.</p>
<p>The issue is further exacerbated by the fact that each construct actually has three variants depending on the compilation phase: the AST is either <em>parsed</em>, <em>renamed</em>, or <em>typechecked</em>, and each construct can have phase-specific extensions.</p>
<p>To make the effort more systematic, we created an automated test case that tracks the current status of the refactoring. If you open <code>testsuite/tests/ghc-api/T25121_status.stdout</code> in GHC’s sources, you will find a detailed report on which representations match or mismatch.</p>
<p>The goal is to make that test case report “match” for every constructor; then we will be able to merge <code>HsType</code> with <code>HsExpr</code>. To that end, here are some constructs that we have already updated for improved uniformity:</p>
<ul>
<li>Literals (dropped <code>HsTyLit</code> in favor of <code>HsLit</code>)</li>
<li>Operators (added infix holes in types)</li>
<li>Wildcards (refactored <code>HsWildCardTy</code> to use <code>HoleKind</code>)</li>
<li>Tuples and sums (updated exact-print annotations)</li>
</ul>
<p>We are looking forward to making more changes in that direction.</p>
<h2 id="the-star-kind-syntax-in-required-type-arguments" tabindex="-1">The star kind syntax in required type arguments</h2>
<p>The <code>StarIsType</code> extension allows the programmer to write <code>*</code> instead of <code>Type</code>:</p>
<pre><code class="hljs language-haskell"><span class="hljs-type">Maybe</span> :: * -&gt; *
</code></pre>
<p>Although it is eventually going to be deprecated per <a href="https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0143-remove-star-kind.rst">GHC Proposal #143</a> “Remove the <code>*</code> kind syntax”, there are currently no plans to fully remove it. Indeed, the proposal uses the following phrasing:</p>
<blockquote>
<p>the <code>-XStarIsType</code> extension <strong>may be removed</strong> from GHC to simplify the internals</p>
</blockquote>
<p>And “may be removed” only hints at the possibility of removal; given the magnitude of the potential breakage, this is not going to happen without friction.</p>
<p>Looking at this from the perspective of unifying term- and type-level parsers, this means we need to parse <code>*</code> in term syntax somehow. The problem is that it conflicts with the multiplication operator! This is a known problem and the raison d’être of the aforementioned proposal.</p>
<p>Back in 2020, we prototyped a solution to this. With a carefully crafted set of edits to the Haskell grammar, it turned out to be possible to add <em>limited</em> support for the <code>*</code> syntax where it does not conflict with multiplication.</p>
<p>Indeed, consider the common kinds:</p>
<ul>
<li><code>Maybe :: * -&gt; *</code></li>
<li><code>Either :: * -&gt; * -&gt; *</code></li>
<li><code>Monad :: (* -&gt; *) -&gt; Constraint</code></li>
</ul>
<p>None of them actually involve the <code>a * b</code> ambiguity.</p>
<p>The prototype that implemented this was actually shelved, because it was difficult to motivate the change to the grammar. But now, with the introduction of <code>RequiredTypeArguments</code>, we can allow the following code:</p>
<pre><code class="hljs language-haskell"><span class="hljs-meta">{-# LANGUAGE RequiredTypeArguments, StarIsType #-}</span>
<span class="hljs-title">x1</span> = f (* -&gt; * -&gt; *)
<span class="hljs-title">x2</span> = f (<span class="hljs-keyword">forall</span> k. k -&gt; *)
<span class="hljs-title">x3</span> = f ((* -&gt; *) -&gt; <span class="hljs-type">Constraint</span>)
</code></pre>
<p>So we dusted off the old patch, rebased and refined it, and now we finally have a sound backwards compatibility story for <code>*</code>. Merging term and type syntax no longer requires its complete removal.</p>
<h2 id="pun-detection-in-required-type-arguments" tabindex="-1">Pun detection in required type arguments</h2>
<p>The initial implementation of <code>RequiredTypeArguments</code> left out pun checking due to engineering difficulties. Consider:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">x</span> = <span class="hljs-number">42</span>

<span class="hljs-title">f</span>, g :: <span class="hljs-keyword">forall</span> a -&gt; ...
<span class="hljs-title">f</span> (<span class="hljs-class"><span class="hljs-keyword">type</span> x) = g x</span>
</code></pre>
<p>In accordance with the specification, the <code>g x</code> function call is renamed as a term, so <code>x</code> refers to the top-level binding <code>x = 42</code>, not to the type variable binding <code>type x</code> as one might expect.</p>
<p>This is somewhat counterintuitive because <code>g</code> expects a type argument. Forbidding puns in required type arguments allows us to produce a helpful error message:</p>
<pre><code class="hljs"><span class="hljs-keyword">error: </span>[GHC<span class="hljs-string">-09591</span>]
  Illegal punned variable occurrence in a required type argument.
  The name ‘x’ could refer to:
    ‘x’ defined at Test.hs:3:1
    ‘x’ bound at Test.hs:5:9
</code></pre>
<p>Unfortunately, the initial attempt to introduce this check was stalled, as the necessary information was not available in the type-checking phase.</p>
<p>Luckily, in an effort to improve error messages, <code>@sheaf</code> corrected the architectural flaw in GHC that was blocking the punning check, and we implemented the check.</p>
<h2 id="new-type-families%3A-tuple%2C-constraints%2C-tuple%23%2C-sum%23" tabindex="-1">New type families: <code>Tuple</code>, <code>Constraints</code>, <code>Tuple#</code>, <code>Sum#</code></h2>
<p><a href="https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0145-non-punning-list-syntax.rst">GHC Proposal #145</a> “Non-punning list and tuple syntax” describes the <code>NoListTuplePuns</code> extension, which removes the overloading of <code>(a, b)</code>: with the extension enabled, <code>(a, b)</code> always refers to the data constructor, never the type constructor. The corresponding tuple type can still be written as <code>Tuple2 a b</code>, but the <code>Tuple</code> type family offers better notation by reusing the familiar <code>(a, b)</code> syntax:</p>
<pre><code class="hljs language-haskell"><span class="hljs-type">Tuple</span> (<span class="hljs-type">Int</span>, <span class="hljs-type">Bool</span>)          = <span class="hljs-type">Tuple2</span> <span class="hljs-type">Int</span> <span class="hljs-type">Bool</span>
<span class="hljs-type">Constraints</span> (<span class="hljs-type">Show</span> a, <span class="hljs-type">Eq</span> a) = <span class="hljs-type">CTuple2</span> (<span class="hljs-type">Show</span> a) (<span class="hljs-type">Eq</span> a)
<span class="hljs-type">Tuple</span># (<span class="hljs-type">Int</span>#, <span class="hljs-type">Float</span>#)      = <span class="hljs-type">Tuple2</span># <span class="hljs-type">Int</span># <span class="hljs-type">Float</span>#
<span class="hljs-type">Sum</span># (<span class="hljs-type">Int</span>#, <span class="hljs-type">Float</span>#)        = <span class="hljs-type">Sum2</span># <span class="hljs-type">Int</span># <span class="hljs-type">Float</span>#
</code></pre>
<p>When the extension was first implemented by <code>@tek</code>, these type families had to be left out because GHC’s type inference was not yet powerful enough to handle them. The missing piece was more aggressive injectivity analysis for closed type families, which <code>@rae</code> proposed and, three years later, <code>@simonpj</code> implemented. With that in place, we were able to add the type families.</p>
<p>As part of this work, we also bumped the maximum sum arity from 63 to 64. Adding the <code>Sum64#</code> constructor had previously been blocked by a separate issue, which <code>@luite</code> resolved.</p>
<p>We also submitted a proposal amendment, as the type family definitions had to be simplified to a form that GHC’s injectivity checker could handle.</p>
<h2 id="rework-of-name-resolution-for-built-in-and-punned-names" tabindex="-1">Rework of name resolution for built-in and punned names</h2>
<p>While working on the name resolution component of GHC, we stumbled upon some outdated documentation and technical debt. Further inspection revealed four lurking bugs:</p>
<ul>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25174">#25174</a> Template Haskell mistakenly assumes <code>&quot;FUN&quot;</code> is built-in syntax</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25179">#25179</a> <code>mkName</code> (template-haskell) ignores <code>NoListTuplePuns</code></li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25180">#25180</a> Valid hole fits don’t suggest tuple constructors</li>
<li><a href="https://gitlab.haskell.org/ghc/ghc/-/issues/25182">#25182</a> <code>MkSolo</code> and <code>MkSolo#</code> are mistakenly classified as <code>BuiltInSyntax</code></li>
</ul>
<p>Fixing them in a principled way called for a complete overhaul of the relevant logic. We cleaned up which names truly qualify as built-in syntax, unified the treatment of tuples of all arities, made the implementation aware of <code>NoListTuplePuns</code>, and updated the internal documentation.</p>
<h2 id="previous-updates-on-dependent-types" tabindex="-1">Previous updates on dependent types</h2>
<ul>
<li><a href="https://serokell.co/blog/serokell-s-work-on-ghc-dependent-types-part-4">Serokell’s Work on GHC: Dependent Types 4</a></li>
<li><a href="https://serokell.co/blog/ghc-dependent-types-in-haskell-3">Serokell’s Work on GHC: Dependent Types 3</a></li>
<li><a href="https://serokell.co/blog/ghc-dependent-types-in-haskell-2">Serokell’s Work on GHC: Dependent Types 2</a></li>
<li><a href="https://serokell.co/blog/ghc-dependent-types-in-haskell">Serokell’s Work on GHC: Dependent Types</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-7-2023-06-14/6444">GHC+DH Weekly Update #7, 2023-06-14 (Discourse)</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-6-2023-06-07/6383">GHC+DH Weekly Update #6, 2023-06-07 (Discourse)</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-5-2023-01-25/5662">GHC+DH Weekly Update #5, 2023-01-25 (Discourse)</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-4-2023-01-18/5608">GHC+DH Weekly Update #4, 2023-01-18 (Discourse)</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-3-2023-01-11/5566">GHC+DH Weekly Update #3, 2023-01-11 (Discourse)</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-2-2022-12-21/5473">GHC+DH Weekly Update #2, 2022-12-21 (Discourse)</a></li>
<li><a href="https://discourse.haskell.org/t/ghc-dh-weekly-update-1-2022-12-07/5418">GHC+DH Weekly Update #1, 2022-12-07 (Discourse)</a></li>
</ul>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>This was the fifth blog post of our series “Work on GHC: Dependent Types”. In this installment, we covered visible <code>forall</code> in GADTs, namespace-specified imports, and the long-awaited fix for type instance ordering in kind checking. We also shared incremental progress on unifying <code>HsType</code> and <code>HsExpr</code>, and a handful of smaller but meaningful improvements throughout the compiler.</p>
<p>We are committed to our vision of dependently-typed programming in Haskell, so stay tuned for future updates!</p>
]]></content:encoded>
            <author>hi+vladislavzavialov@serokell.co (Vladislav Zavialov)</author>
            <enclosure url="https://serokell.co/files/a6/thumb.a6esvf5.normal-work_on_GHC_(1).jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[The Hidden Perils of MonadBaseControl]]></title>
            <link>https://serokell.co/blog/the-hidden-perils-of-monadbasecontrol</link>
            <guid isPermaLink="false">https://serokell.co/blog/the-hidden-perils-of-monadbasecontrol</guid>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[ MonadBaseControl
is notoriously tricky to use correctly. It’s really easy to misuse and
end up introducing subtle unexpected behaviour or downright bugs, even
in the hands of the more experienced de…]]></description>
            <content:encoded><![CDATA[<p><a href="https://hackage.haskell.org/package/monad-control/docs/Control-Monad-Trans-Control.html#t:MonadBaseControl"><code>MonadBaseControl</code></a>
is notoriously tricky to use correctly. It’s really easy to misuse and
end up introducing subtle unexpected behaviour or downright bugs, even
in the hands of the more experienced developers.</p>
<p>The goal of this article is to establish a clear mental model of how to work with <code>MonadBaseControl</code>, recognize its dangers, and how to avoid them.</p>
<p>Lastly, I’ll leave you with recommendations for best practices, when to reach out for <code>MonadBaseControl</code>, and when not to.</p>
<p>This post assumes basic familiarity with <code>MonadBaseControl</code>. If you’ve
never used it, I wholeheartedly recommend reading Alexis King’s
<a href="https://lexi-lambda.github.io/blog/2019/09/07/demystifying-monadbasecontrol/">Demystifying
MonadBaseControl</a>
first and then coming back to this article. I will be reiterating some
of the pitfalls mentioned in her article, expanding upon those, and
diving into others.</p>
<p>Note: this page is available as a <a href="https://github.com/dcastro/dcastro.github.io/blob/master/blog-src/src/Mbc.lhs">Literate Haskell
file</a>.
Refer to that module to find the GHC extensions and imports used
throughout the article.</p>
<p>Before we begin, let’s define a couple of helper functions to make our
examples easier to read:</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | Append a value to the state.</span>
<span class="hljs-title">appendToState</span> :: <span class="hljs-type">MonadState</span> [a] m =&gt; a -&gt; m ()
<span class="hljs-title">appendToState</span> a =
  <span class="hljs-comment">-- For the purpose of this article, excuse the inefficiency.</span>
  <span class="hljs-comment">-- A `DList` or `Seq` would be more appropriate.</span>
  modify (&lt;&gt; [a])

<span class="hljs-comment">-- | Print the current state with a label for context.</span>
<span class="hljs-title">printState</span> :: <span class="hljs-type">Show</span> s =&gt; <span class="hljs-type">String</span> -&gt; <span class="hljs-type">StateT</span> s <span class="hljs-type">IO</span> ()
<span class="hljs-title">printState</span> context = <span class="hljs-keyword">do</span>
  st &lt;- get
  liftIO $ putStrLn $ <span class="hljs-string">&quot;State observed from &#x27;&quot;</span> &lt;&gt; context &lt;&gt; <span class="hljs-string">&quot;&#x27;: &quot;</span> &lt;&gt; show st
</code></pre>
<h2 id="a-quick-refresher" tabindex="-1">A quick refresher</h2>
<p>Say you had this function; it takes an <code>IO</code> action as input and returns
another <code>IO</code> action.</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">foo</span> :: <span class="hljs-keyword">forall</span> a. <span class="hljs-type">IO</span> a -&gt; <span class="hljs-type">IO</span> a
</code></pre>
<p>If we wanted to call <code>foo</code> with an action of type <code>StateT s IO a</code>, we
could “lift” it like this:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">fooState</span> :: <span class="hljs-keyword">forall</span> a s. <span class="hljs-type">StateT</span> s <span class="hljs-type">IO</span> a -&gt; <span class="hljs-type">StateT</span> s <span class="hljs-type">IO</span> a
<span class="hljs-title">fooState</span> stateAction = <span class="hljs-keyword">do</span>
  inputState &lt;- get
  <span class="hljs-keyword">let</span> ioAction = runStateT stateAction inputState :: <span class="hljs-type">IO</span> (a, s)
  (a, outputState) &lt;- liftBase $ foo @(a, s) ioAction
  put outputState
  pure a
</code></pre>
<p>That is, we need to:</p>
<ol>
<li>Use <code>get</code> to capture the input state.</li>
<li>Run the <code>StateT s IO a</code> action with the input state, yielding an
<code>IO (a, s)</code> action.</li>
<li>Call <code>foo</code> with the <code>IO (a, s)</code> action.</li>
<li>Restore the output state with <code>put</code>.</li>
</ol>
<p>Observe how <code>foo</code>’s type parameter is instantiated to <code>@(a, s)</code>,
materializing as <code>foo :: IO (a, s) -&gt; IO (a, s)</code>. In essence, we’re
<em>threading</em> the state through <code>foo</code>, and then restoring it afterwards.</p>
<p><code>MonadBaseControl</code> abstracts over this pattern and provides a
general-purpose way of lifting functions like <code>foo</code> into some
transformer stack.</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">foo&#x27;</span> :: <span class="hljs-keyword">forall</span> m a. (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; m a -&gt; m a
<span class="hljs-title">foo&#x27;</span> action = <span class="hljs-keyword">do</span>
  st &lt;- liftBaseWith \(runInBase :: m a -&gt; <span class="hljs-type">IO</span> (<span class="hljs-type">StM</span> m a)) -&gt; <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">let</span> ioAction = runInBase action :: <span class="hljs-type">IO</span> (<span class="hljs-type">StM</span> m a)
    foo @(<span class="hljs-type">StM</span> m a) ioAction
  restoreM st
</code></pre>
<p>Notice the parallels between this and the <code>StateT</code> version:</p>
<ol>
<li>Use <code>liftBaseWith</code> to capture the input state; this gives us
<code>runInBase</code>, a closure over that state.</li>
<li><code>runInBase</code> will run an <code>m a</code> action with the input state, yielding
an <code>IO (StM m a)</code> action.</li>
<li>Call <code>foo</code> with the <code>IO (StM m a)</code> action¹.</li>
<li>Restore the output state with <code>restoreM</code>.</li>
</ol>
<p>Again, we’re instantiating <code>foo</code> as
<code>foo :: IO (StM m a) -&gt; IO (StM m a)</code>, allowing the state to be threaded
through.</p>
<p>The <code>monad-control</code> package gives us the machinery to do this, and the
packages <code>lifted-base</code> and <code>lifted-async</code> build on top of it to give us
the lifted version of commonly used functions from the <code>base</code> and
<code>async</code> packages, respectively.</p>
<h2 id="discarded-state" tabindex="-1">Discarded state</h2>
<p>To start off simple, let’s take a look at this function:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">whenJust_</span> :: <span class="hljs-type">Maybe</span> b -&gt; (b -&gt; <span class="hljs-type">IO</span> a) -&gt; <span class="hljs-type">IO</span> ()
<span class="hljs-title">whenJust_</span> <span class="hljs-type">Nothing</span> _ = pure ()
<span class="hljs-title">whenJust_</span> (<span class="hljs-type">Just</span> x) f = void $ f x
</code></pre>
<p>A naive first attempt at lifting it might look like this:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">whenJust_&#x27;</span> :: <span class="hljs-keyword">forall</span> m a b. (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; <span class="hljs-type">Maybe</span> b -&gt; (b -&gt; m a) -&gt; m ()
<span class="hljs-title">whenJust_&#x27;</span> mb f = <span class="hljs-keyword">do</span>
  liftBaseWith \runInBase -&gt;
    whenJust_ mb (runInBase . f)
</code></pre>
<p>This typechecks, but it won’t work as expected. Remember: lifting a
function with <code>MonadBaseControl</code> implies threading the state through it,
getting the output state back, and then restoring it.</p>
<p>However, even though <code>whenJust_</code> does take a polymorphic action <code>IO a</code>,
it unfortunately returns <code>IO ()</code>. This means we simply can’t get our
output state back, which means we can’t possibly call <code>restoreM</code>!</p>
<p>As a result, all state modifications will be discarded:</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT testWhenJust1 [0]</span>
<span class="hljs-comment">-- [0]</span>
<span class="hljs-title">testWhenJust1</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">Int</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testWhenJust1</span> = <span class="hljs-keyword">do</span>
  whenJust_&#x27; (<span class="hljs-type">Just</span> <span class="hljs-number">1</span>) \x -&gt; <span class="hljs-keyword">do</span>
    appendToState x
</code></pre>
<p>The only way to preserve state modifications is if <code>whenJust_</code> returns
the <code>a</code> produced by the input action <code>IO a</code>.</p>
<p>In most situations, you can modify the function being lifted, or
reimplement it, to allow the state to flow through. Luckily, in this
instance, that’s fairly easy to fix:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">whenJust</span> :: <span class="hljs-type">Maybe</span> b -&gt; (b -&gt; <span class="hljs-type">IO</span> a) -&gt; <span class="hljs-type">IO</span> (<span class="hljs-type">Maybe</span> a)
<span class="hljs-title">whenJust</span> <span class="hljs-type">Nothing</span> _ = pure <span class="hljs-type">Nothing</span>
<span class="hljs-title">whenJust</span> (<span class="hljs-type">Just</span> x) f = <span class="hljs-type">Just</span> &lt;$&gt; f x

<span class="hljs-title">whenJust_&#x27;&#x27;</span> :: <span class="hljs-keyword">forall</span> m a b. (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; <span class="hljs-type">Maybe</span> b -&gt; (b -&gt; m a) -&gt; m ()
<span class="hljs-title">whenJust_&#x27;&#x27;</span> mb f = <span class="hljs-keyword">do</span>
  stMaybe :: <span class="hljs-type">Maybe</span> (<span class="hljs-type">StM</span> m a) &lt;- liftBaseWith \runInBase -&gt;
    whenJust mb (runInBase . f)
  <span class="hljs-keyword">case</span> stMaybe <span class="hljs-keyword">of</span>
    <span class="hljs-type">Just</span> st -&gt; <span class="hljs-keyword">do</span>
      _ :: a &lt;- restoreM st
      pure ()
    <span class="hljs-type">Nothing</span> -&gt;
      pure ()
</code></pre>
<p>Unlike <code>whenJust_</code>, <code>whenJust</code> does allow <code>a</code> to flow through. The input
action produces an <code>a</code>, which is then wrapped in a <code>Maybe</code> and returned.
We can now capture the output state when <code>Just</code> is returned, and restore
it.</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT testWhenJust2 [0]</span>
<span class="hljs-comment">-- [0,1]</span>
<span class="hljs-title">testWhenJust2</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">Int</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testWhenJust2</span> = <span class="hljs-keyword">do</span>
  whenJust_&#x27;&#x27; (<span class="hljs-type">Just</span> <span class="hljs-number">1</span>) \x -&gt; <span class="hljs-keyword">do</span>
    appendToState x
</code></pre>
<h2 id="threading-state" tabindex="-1">Threading state</h2>
<p>While lifting a function with 1 input action is usually straightforward,
lifting a function with 2 or more can get really thorny. Let’s have a
look at a more nuanced (and rather contrived) example and try lifting
<code>logDuration</code>:</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; logDuration (threadDelay 1_e6) (\d -&gt; putStrLn $ &quot;Took &quot; &lt;&gt; show d)</span>
<span class="hljs-comment">-- Took ...s</span>
<span class="hljs-title">logDuration</span> :: <span class="hljs-type">IO</span> a -&gt; (<span class="hljs-type">NominalDiffTime</span> -&gt; <span class="hljs-type">IO</span> b) -&gt; <span class="hljs-type">IO</span> a
<span class="hljs-title">logDuration</span> action logFn = <span class="hljs-keyword">do</span>
  (a, duration) &lt;- timed action
  _ &lt;- logFn duration
  pure a

<span class="hljs-title">timed</span> :: <span class="hljs-type">IO</span> a -&gt; <span class="hljs-type">IO</span> (a, <span class="hljs-type">NominalDiffTime</span>)
</code></pre>
<p>To avoid the trap described in the last section, we’re going to be using
the higher-order combinator
<a href="https://hackage.haskell.org/package/monad-control-1.0.3.1/docs/Control-Monad-Trans-Control.html#v:control"><code>control</code></a>,
which ensures we <em>do</em> call <code>restoreM</code>.</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">logDuration&#x27;</span> :: (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; m a -&gt; (<span class="hljs-type">NominalDiffTime</span> -&gt; m b) -&gt; m a
<span class="hljs-title">logDuration&#x27;</span> action logFn = <span class="hljs-keyword">do</span>
  control \runInBase -&gt;
    logDuration (runInBase action) (runInBase . logFn)
</code></pre>
<p>However, there are 2 problems with this.</p>
<p>First, the input state is being forked and passed into both <code>action</code> and
<code>logFn</code>. Recall that <code>runInBase</code> is a closure that captures the input
state. And because we’re applying it twice, once to <code>action</code> and once to
<code>logFn</code>, both actions will see the same input state.</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; evalStateT testLogDuration1 [0]</span>
<span class="hljs-comment">-- State observed from &#x27;action&#x27;: [0]</span>
<span class="hljs-comment">-- State observed from &#x27;logFn&#x27;: [0]</span>
<span class="hljs-title">testLogDuration1</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">Int</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testLogDuration1</span> = <span class="hljs-keyword">do</span>
  logDuration&#x27;
    (printState <span class="hljs-string">&quot;action&quot;</span> &gt;&gt; appendToState <span class="hljs-number">1</span>)
    (\_ -&gt; printState <span class="hljs-string">&quot;logFn&quot;</span>)
</code></pre>
<p>This is not the behaviour most users would expect. A more sensible
implementation would thread the output state of <code>action</code> into <code>logFn</code>.</p>
<p>The second problem is that, even though we <em>are</em> using <code>restoreM</code> to
restore the output state, we’re only restoring the output state of
<code>action</code>. The output state of <code>logFn</code> is being discarded.</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT testLogDuration2 [0]</span>
<span class="hljs-comment">-- [0]</span>
<span class="hljs-title">testLogDuration2</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">Int</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testLogDuration2</span> = <span class="hljs-keyword">do</span>
  logDuration&#x27;
    (pure ())
    (\_ -&gt; appendToState <span class="hljs-number">1</span>)
</code></pre>
<p>Why? Let’s have a closer look at the type of <code>logDuration</code>. Note how it
takes 2 input actions, <code>IO a</code> and <code>NominalDiffTime -&gt; IO b</code>, but only
returns the output <code>a</code> of the first action. <code>b</code> is never returned, so
its state cannot be restored.</p>
<p>Just as before, we need to break the function apart and reimplement it
in terms of its primitives.</p>
<p><code>logDuration</code> is defined in terms of <code>timed</code>, which takes a single input
action. Its type is <code>IO a -&gt; IO (a, NominalDiffTime)</code>, so it allows the
state to flow through.</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">logDuration&#x27;&#x27;</span> :: (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; m a -&gt; (<span class="hljs-type">NominalDiffTime</span> -&gt; m b) -&gt; m a
<span class="hljs-title">logDuration&#x27;&#x27;</span> action logFn = <span class="hljs-keyword">do</span>
  (st, duration) &lt;- liftBaseWith \runInBase -&gt; <span class="hljs-keyword">do</span>
    timed (runInBase action)
  a &lt;- restoreM st
  _ &lt;- logFn duration
  pure a
</code></pre>
<p>Now the input state is observed by <code>action</code>, <code>action</code>’s output state is
observed by <code>logFn</code>, and <code>logFn</code>’s output state will be observed by the
caller.</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT testLogDuration3 [0]</span>
<span class="hljs-comment">-- State observed from &#x27;action&#x27;: [0]</span>
<span class="hljs-comment">-- State observed from &#x27;logFn&#x27;: [0,1]</span>
<span class="hljs-comment">-- [0,1,2]</span>
<span class="hljs-title">testLogDuration3</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">Int</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testLogDuration3</span> = <span class="hljs-keyword">do</span>
  logDuration&#x27;&#x27;
    (printState <span class="hljs-string">&quot;action&quot;</span> &gt;&gt; appendToState <span class="hljs-number">1</span>)
    (\_ -&gt; printState <span class="hljs-string">&quot;logFn&quot;</span> &gt;&gt; appendToState <span class="hljs-number">2</span>)
</code></pre>
<h2 id="brick-walls" tabindex="-1">Brick walls</h2>
<p>So far, we’ve learned to be mindful of how the state flows through the
function, how it’s captured and restored, and how we can “massage” the
function’s definition to make things work.</p>
<p>Still, there are times when we’ll hit a wall and find functions that are
just impossible to lift with <code>MonadBaseControl</code> in a satisfactory way.</p>
<p>Two very common pitfalls are functions related to concurrency and
exception handling.</p>
<h3 id="concurrently">
<p>concurrently</p>
</h3>
<p>Concurrency is a rather obvious issue, and
<a href="https://hackage.haskell.org/package/async/docs/Control-Concurrent-Async.html#v:concurrently"><code>concurrently</code></a>
illustrates it well.</p>
<pre><code class="hljs language-hs"><span class="hljs-title">concurrently</span> :: <span class="hljs-type">IO</span> a -&gt; <span class="hljs-type">IO</span> b -&gt; <span class="hljs-type">IO</span> (a, b)
</code></pre>
<p>At a fundamental level, the input state must be forked and given to both
branches and, once they’re done, we must only keep the state of one
branch.</p>
<p>In the implementation below, I arbitrarily chose to always keep the
state of the second branch, <em>regardless of which action finishes first</em>.
This is exactly how <code>concurrently</code> is implemented in the <code>lifted-async</code>
package.</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT (concurrently&#x27; (appendToState 1) (appendToState 2)) []</span>
<span class="hljs-comment">-- [2]</span>
<span class="hljs-title">concurrently&#x27;</span> :: (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; m a -&gt; m b -&gt; m (a, b)
<span class="hljs-title">concurrently&#x27;</span> ma mb = <span class="hljs-keyword">do</span>
  (stateA, stateB) &lt;- liftBaseWith \runInBase -&gt; <span class="hljs-keyword">do</span>
    <span class="hljs-type">Async</span>.withAsync (runInBase ma) \asyncA -&gt;
      <span class="hljs-type">Async</span>.withAsync (runInBase mb) \asyncB -&gt; <span class="hljs-keyword">do</span>
        <span class="hljs-type">Async</span>.waitBoth asyncA asyncB

  a &lt;- restoreM stateA <span class="hljs-comment">-- here we restore the output state of the 1st branch, but then...</span>
  b &lt;- restoreM stateB <span class="hljs-comment">-- ... we immediately overwrite it with the output state of the 2nd branch.</span>
  pure (a, b)
</code></pre>
<h3 id="bracket">
<p>bracket</p>
</h3>
<p>The issues with mixing exception handling and <code>MonadBaseControl</code> are a
bit more subtle and deceiving. Let’s have a look at <code>bracket</code> to
understand why.</p>
<pre><code class="hljs language-hs"><span class="hljs-title">bracket</span> :: <span class="hljs-type">IO</span> a -&gt; (a -&gt; <span class="hljs-type">IO</span> b) -&gt; (a -&gt; <span class="hljs-type">IO</span> c) -&gt; <span class="hljs-type">IO</span> c
</code></pre>
<p>If we start out with <code>control</code> and “follow the types”, we’ll get this:</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">bracket&#x27;</span> :: (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; m a -&gt; (a -&gt; m b) -&gt; (a -&gt; m c) -&gt; m c
<span class="hljs-title">bracket&#x27;</span> acquire release use =
  control $ \runInBase -&gt;
    bracket
      (runInBase acquire)
      (\st -&gt; runInBase $ restoreM st &gt;&gt;= release)
      (\st -&gt; runInBase $ restoreM st &gt;&gt;= use)
</code></pre>
<p>In fact, this is the exact example given in the docs for
<a href="https://hackage.haskell.org/package/monad-control-1.0.3.1/docs/Control-Monad-Trans-Control.html#v:control"><code>control</code></a>.</p>
<p>To understand it, it helps to see how exactly <code>bracket</code>’s type
parameters are being instantiated here:</p>
<pre><code class="hljs language-hs"><span class="hljs-title">bracket</span>
  :: <span class="hljs-type">IO</span> (<span class="hljs-type">StM</span> m a)              <span class="hljs-comment">-- acquire</span>
  -&gt; (<span class="hljs-type">StM</span> m a -&gt; <span class="hljs-type">IO</span> (<span class="hljs-type">StM</span> m b)) <span class="hljs-comment">-- release</span>
  -&gt; (<span class="hljs-type">StM</span> m a -&gt; <span class="hljs-type">IO</span> (<span class="hljs-type">StM</span> m c)) <span class="hljs-comment">-- use</span>
  -&gt; <span class="hljs-type">IO</span> (<span class="hljs-type">StM</span> m c)
</code></pre>
<p>Let’s break it down:</p>
<ul>
<li>The input state is captured and passed to our <code>acquire</code> action.</li>
<li>The output state of <code>acquire</code> (<code>StM m a</code>) is passed to both <code>use</code> and
<code>release</code>; <code>restoreM st &gt;&gt;= ...</code> ensures our <code>use</code> and <code>release</code>
actions will see it.</li>
<li>The output state of <code>release</code> (<code>StM m b</code>) is discarded.</li>
<li>The output state of <code>use</code> (<code>StM m c</code>) is returned by <code>bracket</code> and
will be restored by <code>control</code>.</li>
</ul>
<p>There are 2 issues with this implementation:</p>
<ul>
<li><code>release</code> runs after <code>acquire</code> and <code>use</code>, but <em>only</em> sees the output
state of <code>acquire</code>, not <code>use</code>.</li>
<li>The output state of <code>release</code> is discarded.</li>
</ul>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT testBracket1 []</span>
<span class="hljs-comment">-- State observed from &#x27;release&#x27;: [&quot;acquire&quot;]</span>
<span class="hljs-comment">-- [&quot;acquire&quot;,&quot;use&quot;]</span>
<span class="hljs-title">testBracket1</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">String</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testBracket1</span> =
  bracket&#x27;
    (appendToState <span class="hljs-string">&quot;acquire&quot;</span>)
    (\_ -&gt; printState <span class="hljs-string">&quot;release&quot;</span> &gt;&gt; appendToState <span class="hljs-string">&quot;release&quot;</span>)
    (\_ -&gt; appendToState <span class="hljs-string">&quot;use&quot;</span>)
</code></pre>
<p>This is not how we want the state to be threaded. Again, we’ll break the
function apart and reimplement it in terms of its primitives. <a href="https://hackage.haskell.org/package/ghc-internal-9.1201.0/docs/src/GHC.Internal.Control.Exception.Base.html#bracket"><code>bracket</code>
is
defined</a>
using <code>mask</code> and <code>onException</code>, so we’ll redefine it using lifted
versions of those same functions from the <code>lifted-base</code> package (which
do behave sensibly).</p>
<pre><code class="hljs language-haskell"><span class="hljs-title">bracket&#x27;&#x27;</span> :: (<span class="hljs-type">MonadBaseControl</span> <span class="hljs-type">IO</span> m) =&gt; m a -&gt; (a -&gt; m b) -&gt; (a -&gt; m c) -&gt; m c
<span class="hljs-title">bracket&#x27;&#x27;</span> acquire release use =
  <span class="hljs-type">Lifted</span>.mask \restore -&gt; <span class="hljs-keyword">do</span>
    a &lt;- acquire
    c &lt;- restore (use a) `<span class="hljs-type">Lifted</span>.onException` release a
    _ &lt;- release a
    pure c
</code></pre>
<p>Now we can observe the state being threaded correctly through all 3
actions:</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; execStateT testBracket2 []</span>
<span class="hljs-comment">-- State observed from &#x27;acquire&#x27;: []</span>
<span class="hljs-comment">-- State observed from &#x27;use&#x27;: [&quot;acquire&quot;]</span>
<span class="hljs-comment">-- State observed from &#x27;release&#x27;: [&quot;acquire&quot;,&quot;use&quot;]</span>
<span class="hljs-comment">-- [&quot;acquire&quot;,&quot;use&quot;,&quot;release&quot;]</span>
<span class="hljs-title">testBracket2</span> :: <span class="hljs-type">StateT</span> [<span class="hljs-type">String</span>] <span class="hljs-type">IO</span> ()
<span class="hljs-title">testBracket2</span> =
  bracket&#x27;&#x27;
    (printState <span class="hljs-string">&quot;acquire&quot;</span> &gt;&gt; appendToState <span class="hljs-string">&quot;acquire&quot;</span>)
    (\_ -&gt; printState <span class="hljs-string">&quot;release&quot;</span> &gt;&gt; appendToState <span class="hljs-string">&quot;release&quot;</span>)
    (\_ -&gt; printState <span class="hljs-string">&quot;use&quot;</span> &gt;&gt; appendToState <span class="hljs-string">&quot;use&quot;</span>)
</code></pre>
<p>Looking good, right? Well… there’s a very insidious bug hiding in there.</p>
<p>It works great when run on <code>StateT</code>, but if we run it on <code>ExceptT</code>, we
run into trouble. If the <code>use</code> function exits with <code>throwError</code>, then
that effect will cause <code>bracket''</code> to short-circuit and skip the
<code>release</code> handler!</p>
<pre><code class="hljs language-haskell"><span class="hljs-comment">-- | &gt;&gt;&gt; runExceptT testBracketExcept</span>
<span class="hljs-comment">-- acquire</span>
<span class="hljs-comment">-- use</span>
<span class="hljs-comment">-- Left &quot;use error&quot;</span>
<span class="hljs-title">testBracketExcept</span> :: <span class="hljs-type">ExceptT</span> <span class="hljs-type">String</span> <span class="hljs-type">IO</span> ()
<span class="hljs-title">testBracketExcept</span> = <span class="hljs-keyword">do</span>
  bracket&#x27;&#x27;
    (liftIO (putStrLn <span class="hljs-string">&quot;acquire&quot;</span>))
    (\_ -&gt; liftIO (putStrLn <span class="hljs-string">&quot;release&quot;</span>))
    (\_ -&gt; liftIO (putStrLn <span class="hljs-string">&quot;use&quot;</span>) &gt;&gt; throwError <span class="hljs-string">&quot;use error&quot;</span>)
</code></pre>
<p>The <code>use</code> function does not throw an exception (so
<code>`Lifted.onException` release a</code> is never run) and exits early
(before <code>_ &lt;- release a</code> has a chance to run).</p>
<p>In an attempt to fix the threading of the state, we ended up making
things much worse and broke <code>bracket</code>’s semantics!</p>
<p>The issue here is that we want the output state of <code>use</code> to be passed to
<code>release</code>, <em>except</em> when dealing with transformers with multiple exit
points like <code>ExceptT</code> and <code>MaybeT</code>.</p>
<p>In the latter case, in order to preserve bracket’s semantics, we want
to:</p>
<ol>
<li>Run <code>use</code>, but don’t restore its output state yet.</li>
<li>Run <code>release</code>.</li>
<li>If both <code>use</code> and <code>release</code> exited with an error, rethrow
<code>release</code>’s.</li>
<li>Otherwise, restore <code>use</code>’s output state.</li>
</ol>
<p>We simply cannot do this with <code>MonadBaseControl</code>. A function lifted with
<code>MonadBaseControl</code> has to capture/restore state uniformly for all
possible transformers. But what we want here is to have <em>multiple</em>
implementations of <code>bracket</code>, one for each concrete monad transformer,
that decides how best to capture/restore state.</p>
<p>And that is precisely how the <code>exceptions</code> package solves this problem.
It defines a <code>MonadMask</code> typeclass that, among other things, describes
the semantics of an abstract
<a href="https://hackage-content.haskell.org/package/exceptions/docs/Control-Monad-Catch.html#v:generalBracket"><code>generalBracket</code></a>.
The instances for <code>StateT</code>, <code>ExceptT</code>, <code>MaybeT</code>, etc., then implement
the correct state threading behaviour for each transformer, while
upholding the prescribed semantics.</p>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>The good ol’ “if it compiles, it works” just doesn’t apply when dealing
with <code>MonadBaseControl</code>. It’s not just a matter of making the types line
up; it’s a matter of semantics.</p>
<p>If you can get away with using only stateless transformers, do it! None
of the issues described here apply to stateless transformers (e.g.,
<code>ReaderT</code>, <code>LogT</code>). Forking state is innocuous, and there’s no output
state to restore afterwards.</p>
<p>You often can avoid stateful transformers by replacing <code>StateT s</code> with
mutable variables such as <code>ReaderT (IORef s)</code>, and <code>ExceptT</code> with
runtime exceptions.</p>
<p>You can constrain your functions with <code>StM m a ~ a</code> to rule out stateful
transformers. This is what the “safe” module
<a href="https://hackage-content.haskell.org/package/lifted-async/docs/Control-Concurrent-Async-Lifted-Safe.html"><code>Control.Concurrent.Async.Lifted.Safe</code></a>
from the <code>lifted-async</code> package does, and you should prefer it over
<code>Control.Concurrent.Async.Lifted</code>.</p>
<p>Another alternative is
<a href="https://hackage.haskell.org/package/unliftio"><code>MonadUnliftIO</code></a>. It’s
roughly equivalent to <code>MonadBaseControl</code> with <code>StM m a ~ a</code>, but with a
simpler API. The downside is that the base monad is constrained to <code>IO</code>.</p>
<p>Nevertheless, if you must support stateful transformers (e.g., <code>StateT</code>,
<code>ExceptT</code>, <code>MaybeT</code>), lifting functions with only 1 input action, like
<code>withMVar</code>, is usually easy enough. Just remember to <em>always</em> restore
the output state using <code>restoreM</code>. Prefer using higher-order combinators
like <code>control</code> and <code>liftBaseOp</code>, if possible.</p>
<p>Lifting functions with 2 or more input actions, on the other hand, is
when things get complicated. Having to use the <code>runInBase</code> closure more
than once is a dead giveaway that something might be off. Reimplementing
the function in terms of simpler functions that only take 1 input action
each <em>can</em> sometimes work, but it’s not guaranteed.</p>
<p>For exception handling, I’d recommend avoiding <code>MonadBaseControl</code> and
<code>lifted-base</code>. Instead, go with the <code>exceptions</code> package or, better yet,
<code>safe-exceptions</code> for <a href="https://github.com/fpco/safe-exceptions?tab=readme-ov-file#goals">safer handling of async
exceptions</a>.
You get the best of both worlds: power (it supports stateful
transformers) and safety (it behaves sensibly with regard to state).</p>
<hr>
<p><code>MonadBaseControl</code> is a big hammer; wield it wisely.</p>
<hr>
<p>¹: <code>StM m a</code> is an associated type family of <code>MonadBaseControl</code>. It
represents a value <code>a</code> enriched with the state of a monad <code>m</code>. For
example, <code>StM (StateT s IO) a ~ (a, s)</code>, and
<code>StM (ExceptT e IO) a ~ Either e a</code>. For stateless monads, <code>StM m a</code>
evaluates to <code>a</code>: <code>StM (ReaderT r IO) a ~ a</code>.</p>
]]></content:encoded>
            <author>hi+diogocastro@serokell.co (Diogo Castro)</author>
            <enclosure url="https://serokell.co/files/ah/thumb.ahyhr2h.normal-The_Hidden_Perils_of_MonadBaseControl.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Rust in Production: JetBrains]]></title>
            <link>https://serokell.co/blog/rust-in-production-jetbrains</link>
            <guid isPermaLink="false">https://serokell.co/blog/rust-in-production-jetbrains</guid>
            <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[ In our Rust in Production interview series, we talk with developers and technical leaders who are shaping how Rust is built and used in practice..]]></description>
            <content:encoded><![CDATA[<p>In our Rust in Production interview series, we talk with developers and technical leaders who are shaping how Rust is built and used in practice.</p>
<p>This interview explores JetBrains’ strategy for supporting the Rust Foundation and collaborating around shared tooling like rust-analyzer, the rationale behind launching RustRover, and how user adoption data shapes priorities such as debugging, async Rust workflows, and test tooling (including cargo nextest).</p>
<p>Today’s guest is the Head of the Rust Ecosystem at JetBrains, <a href="https://bravit.pro/">Vitaly Bragilevsky</a>.<br>
<img src="https://serokell.co/files/ar/arua069.personal_card-Vitaly_Bragilevsky_(1).jpg" alt="arua069.personal_card-Vitaly_Bragilevsky_(1).jpg"></p>
<h3 id="in-talks-and-interviews-you-often-emphasize-jetbrains%E2%80%99-long-term-commitment-to-the-rust-ecosystem%2C-including-your-involvement-in-the-rust-foundation-and-collaboration-around-rust-analyzer.-how-do-you-balance-building-proprietary-jetbrains-tooling-with-contributing-to-shared%2C-community-owned-rust-infrastructure-in-a-way-that-actually-accelerates-rust-adoption-rather-than-fragmenting-the-tooling-landscape%3F" tabindex="-1">In talks and interviews you often emphasize JetBrains’ long-term commitment to the Rust ecosystem, including your involvement in the Rust Foundation and collaboration around rust-analyzer. How do you balance building proprietary JetBrains tooling with contributing to shared, community-owned Rust infrastructure in a way that actually accelerates Rust adoption rather than fragmenting the tooling landscape?</h3>
<p>We think about this balance very deliberately, because the last thing the Rust ecosystem needs is artificial fragmentation driven by vendors pulling in different directions.</p>
<p>First, it’s important to be precise about our role. We don’t directly contribute code to core Rust open-source projects like rust-analyzer, but we very much share the same underlying problems. Because of that, we stay in <a href="https://2026.rustweek.org/talks/ides/">close communication with the rust-analyzer team</a>, exchange feedback, and align in direction where it makes sense. In parallel, we participate in Rust Foundation programs focused on supporting the people and processes behind Rust development, not on controlling technology.</p>
<p>From a product perspective, our default stance is: use what the Rust ecosystem already provides whenever possible. We rely on the standard Rust toolchain and regularly evaluate which existing components can be reused or integrated into RustRover rather than reinventing them. This helps us stay compatible with how Rust developers already work and lowers the cognitive cost of adopting the language.</p>
<p>Regarding fragmentation, I see it a bit differently. Diversity of tools and approaches isn’t a failure mode by default – it’s often a strength. Different developers, teams, and domains need different workflows. A strictly limited, highly opinionated tooling setup may be elegant, but it can also exclude people. Healthy competition and multiple well-integrated tools give users real choice, and that choice is what ultimately accelerates adoption.</p>
<p>Our goal is not to replace or overshadow community-owned infrastructure, but to build on top of it and around it: providing a polished, integrated experience for users who value that, while staying aligned with the broader ecosystem. When developers can pick the tools that fit them best – whether that’s lightweight editors, full IDEs, or something in between – Rust becomes more accessible, not more fragmented.</p>
<h3 id="rustrover-is-jetbrains%E2%80%99-first-dedicated-rust-ide-after-years-of-offering-rust-support-as-plugins-for-intellij-idea-and-clion.-from-your-perspective-as-head-of-the-rust-ecosystem%2C-what-concrete-signs-inside-jetbrains-and-in-the-wider-community-convinced-you-that-the-time-had-come-to-invest-in-a-standalone-rust-product-rather-than-%E2%80%9Cjust%E2%80%9D-improving-the-plugins%3F" tabindex="-1">RustRover is JetBrains’ first dedicated Rust IDE after years of offering Rust support as plugins for IntelliJ IDEA and CLion. From your perspective as Head of the Rust Ecosystem, what concrete signs inside JetBrains and in the wider community convinced you that the time had come to invest in a standalone Rust product rather than “just” improving the plugins?</h3>
<p>The decision was driven by a combination of external signals from the Rust ecosystem and very practical internal considerations at JetBrains.</p>
<p>Externally, we saw sustained, long-term growth of Rust – not just in developer adoption, but in serious production use by companies across very different industries. More teams were betting on Rust for core systems, making the ecosystem commercially relevant in a way that clearly went beyond enthusiasts and early adopters. At that point, simply treating Rust as an add-on to other IDEs no longer reflected how important it had become for many users.</p>
<p>Internally, having a standalone product matters a lot. It allows us to put Rust development on a clear roadmap, dedicate a focused team, and invest in the experience end-to-end rather than competing for attention and resources within a broader product. From an organizational and product-management perspective, this is a much healthier way to build something long-term.</p>
<p>There’s also a signaling effect that I personally consider very important. When JetBrains launches a dedicated, commercial IDE for a language, it sends a strong message to companies: this technology is mature, well-supported, and a safe bet. In that sense, RustRover is not only a response to Rust’s growth – it’s also a way of reinforcing it, giving teams additional confidence that Rust is ready for broader adoption in professional environments.</p>
<h3 id="jetbrains-surveys-show-a-steady-rise-in-rust-usage-and-in-the-popularity-of-advanced-ide-tooling-such-as-rust-analyzer-and-intellij-rust.-how-is-this-data-influencing-your-internal-roadmap%3A-which-rust-adoption-patterns-among-your-users-most-directly-shape-what-your-team-prioritizes-in-rustrover-and-related-tools%3F" tabindex="-1">JetBrains surveys show a steady rise in Rust usage and in the popularity of advanced IDE tooling such as rust-analyzer and IntelliJ Rust. How is this data influencing your internal roadmap: which Rust adoption patterns among your users most directly shape what your team prioritizes in RustRover and related tools?</h3>
<h3 id="" tabindex="-1"></h3>
<p>For us, surveys are not just about measuring popularity — they’re a way to understand how Rust is actually used in practice and where developers are still paying a lot of friction tax.</p>
<p>The most valuable signals for us are things like the industries where Rust is being adopted, the kinds of applications people are building, and the tooling problems they repeatedly run into. These answers have a very direct impact on our roadmap, because they tell us where better tooling can realistically move the needle for adoption and productivity.</p>
<p>A concrete example is debugging. We consistently see that many Rust developers avoid debuggers altogether, not because they don’t need them, but because the existing experience is often unreliable or hard to use. That’s a clear signal for us to invest more heavily in debugger quality and integration, rather than assuming that “Rust developers just don’t debug.”</p>
<p>We also see that a large share of Rust development today is backend work, with heavy use of asynchronous Rust. That has consequences for everything from code insight and diagnostics to debugging and profiling, and it means we need to focus on improving the developer experience specifically for async-heavy scenarios, not just for small libraries or toy examples.</p>
<p>Finally, the surveys show a significant cluster of Rust usage in areas like blockchain — for example, ecosystems such as Solana. For us, this isn’t a niche curiosity; it’s a signal that real teams are building production systems there, and that investing in better support for these workflows can have a tangible impact. In that sense, our roadmap is shaped less by abstract ideas of what Rust could be used for, and more by careful observation of what Rust developers are already doing today — and where better tools can help them do it with less friction.</p>
<h3 id="for-someone-who-uses-zed%2Fneovim-with-a-rust-analyzer%2C-what-difference-would-you-feel-with-rustrover.-is-the-proprietary-jb-engine-better-than-the-tools-rust-provides-and-what%E2%80%99s-the-story-for-a-proprietary-engine-instead-of-a-community-driven-analyzer%3F" tabindex="-1"><strong>For someone who uses Zed/Neovim with a rust-analyzer, what difference would you feel with RustRover. Is the proprietary JB engine better than the tools rust provides and what’s the story for a proprietary engine instead of a community-driven analyzer?</strong></h3>
<p>If you’re coming from Zed or Neovim with rust-analyzer, the first difference you’ll notice is that RustRover is not “just” code analysis – it’s a full IDE experience that’s available out of the box and designed to work as a coherent system.</p>
<p>RustRover combines Rust code analysis with a lot of other IDE capabilities: a debugger, a profiler, advanced dependency management, collaboration tools, support for web technologies and databases, and AI features ranging from simple model-based interactions to more advanced agent-style workflows. On the debugging side in particular, we use our own forks of LLDB and GDB, tuned specifically for Rust, because upstream debuggers still struggle with many Rust-specific constructs.</p>
<p>I usually try not to frame this as a direct “rust-analyzer vs. JetBrains engine” comparison. Both analyzers cover a broadly similar feature set, and both have rough edges – Rust is a very complex language, and large real-world codebases stress tools in different ways. Depending on project size, architecture, macros, build setup, and many other factors, developers can get noticeably different results from different analyzers.</p>
<p>Our engine has a long history. It started about ten years ago as part of the IntelliJ Rust plugin, well before rust-analyzer existed, and it was built using the traditional JetBrains approach to deep IDE integration. Interestingly, it was originally started by Aleksey Kladov (matklad) – the same person who later initiated rust-analyzer, which is based on very different architectural principles.</p>
<p>Today, we don’t see a strong reason to abandon our own analysis stack. One major advantage is that we’re much closer to the IDE itself: we’re not constrained by the LSP protocol, and we can build UX features that simply aren’t possible when the analyzer is a separate, generic service. That tight integration enables things like richer refactorings, more context-aware navigation, and smoother interactions across debugging, profiling, and code insight.</p>
<p>Finally, I actually think it’s healthy that Rust has two serious analyzers. It means no one can afford to be complacent. The competition pushes both approaches forward – and in the end, Rust developers are the ones who benefit from that constant pressure to improve.</p>
<h3 id="jetbrains-actively-supports-the-rust-foundation.-what-motivated-this-decision%2C-and-what-kind-of-value-does-jetbrains-expect-to-get-back%3F" tabindex="-1"><strong>JetBrains actively supports the Rust Foundation. What motivated this decision, and what kind of value does JetBrains expect to get back?</strong></h3>
<p>Joining the Rust Foundation was a very natural step for us at the time. As Rust was entering a more mature phase of adoption, the Foundation emerged as a focal point for coordinating long-term, ecosystem-level efforts: supporting core infrastructure, improving the sustainability of key projects, investing in developer education, and providing a neutral space where companies and the community can work together.</p>
<p>For JetBrains, participation in the Rust Foundation gives us a structured and transparent way to engage at that level. We get the opportunity to talk directly with companies that are deeply invested in Rust, to contribute to discussions about priorities and initiatives, and to propose or support programs that improve the overall developer experience. While the Foundation doesn’t dictate technical direction, it plays an important role in aligning efforts around shared problems that no single company can solve alone.</p>
<p>From a practical standpoint, working with an organization like the Rust Foundation is also simply more convenient and scalable for us as a company. We still communicate with individual open-source projects and with members of the Rust community directly, but the Foundation gives us a central forum where those conversations can happen more systematically and with broader impact.</p>
<p>Ultimately, the value we expect to get back is not a specific technical advantage, but a healthier, more sustainable Rust ecosystem. That directly benefits our users – and, by extension, our products – because better infrastructure, better-supported maintainers, and clearer long-term signals make Rust a safer and more attractive choice for teams and companies.</p>
<h3 id="there-is-a-fairly-common-perception-in-the-community-that-testing-in-rust-can-feel-less-flexible-and-more-verbose-%E2%80%93-especially-when-it-comes-to-mocks%2C-fixtures%2C-and-test-infrastructure%2C-which-often-rely-on-third-party-crates-and-careful-architectural-design.-do-you-agree-that-this-is-a-real-pain-point-for-rust-developers-today%3F-how-do-you-see-this-area-evolving%2C-and-is-improving-test-ergonomics-something-that-the-language-team-and-tooling-vendors-like-jetbrains-are-actively-focusing-on%3F" tabindex="-1"><strong>There is a fairly common perception in the community that testing in Rust can feel less flexible and more verbose – especially when it comes to mocks, fixtures, and test infrastructure, which often rely on third-party crates and careful architectural design. Do you agree that this is a real pain point for Rust developers today? How do you see this area evolving, and is improving test ergonomics something that the language team and tooling vendors like JetBrains are actively focusing on?</strong></h3>
<p>I think <code>cargo nextest</code> is a great example of how the Rust ecosystem is evolving to address real testing pain points without overloading the language itself. Rust deliberately keeps its built-in testing model minimal and reliable, but that means that questions of scale – performance, isolation, flaky tests, CI ergonomics – are pushed into external tooling. As projects grow, <code>cargo test</code> often becomes a bottleneck, and that’s exactly the space where <code>nextest</code> provides a much more robust and production-ready test execution model.</p>
<p>What’s important is that <code>nextest</code> doesn’t change how tests are written in Rust at all. All the existing approaches – standard <code>#[test]</code>, async tests, fixtures and mocks from third-party crates – continue to work as they are. Instead, it focuses on execution, observability, and control, which are some of the biggest sources of friction for teams working with large Rust codebases. In that sense, it complements the existing ecosystem rather than competing with it.</p>
<p>From a tooling perspective, this is exactly where IDEs can add a lot of value. We see strong demand for better test workflows, and that’s why we’re actively working on deeper <a href="https://youtrack.jetbrains.com/issue/RUST-12459/Support-cargo-nextest-test-runner">integration of cargo nextest into RustRover</a> – including running, debugging, and visualizing test results. Our goal is to hide as much of the infrastructural complexity as possible behind a coherent UX, so developers can benefit from powerful tools like <code>nextest</code> without having to constantly think about how all the pieces are wired together.</p>
]]></content:encoded>
            <author>hi+ivangromakovsky@serokell.co (Ivan Gromakovsky)</author>
            <enclosure url="https://serokell.co/files/aj/thumb.ajibif6.normal-Haskell_in_production_JetBrains_(1).jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Beyond the Hype: Crossing the GenAI Divide in Real-World Business]]></title>
            <link>https://serokell.co/blog/beyond-the-hype-crossing-the-genai-divide-in-real-world-business</link>
            <guid isPermaLink="false">https://serokell.co/blog/beyond-the-hype-crossing-the-genai-divide-in-real-world-business</guid>
            <pubDate>Wed, 24 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[ If you judged the state of AI in business by your LinkedIn feed, you’d think the revolution already happened..]]></description>
            <content:encoded><![CDATA[<p>If you judged the state of AI in business by your LinkedIn feed, you’d think the revolution already happened.</p>
<p>Boards ask about GenAI at every quarterly review. Teams are spinning up pilots. Vendors are promising “copilots” for every function, from procurement to payroll. And yet, when you look at the P&amp;L, a quieter story emerges: for most organizations, nothing fundamental has changed.</p>
<p>Hard numbers come from the recent MIT-backed study on the State of AI in Business 2025. Despite an estimated $30-40 billion being poured into GenAI, 95% of organizations are seeing no measurable return. Only 5% of integrated AI pilots deliver real, meaningful value.</p>
<p>The report terms this chasm the GenAI Divide, and for anyone building or buying AI, it is the defining challenge of the next few years.</p>
<p>As a company that designs and implements AI solutions for enterprises, we see this divide every day: between demos and deployment, between experimentation and transformation, between tools that impress in slide decks and systems that quietly move business metrics.</p>
<p>This essay is about what that divide really is and how to cross it.</p>
<h2 id="high-adoption%2C-low-transformation" tabindex="-1">High Adoption, Low Transformation</h2>
<p>On paper, at least, the adoption of GenAI is booming: more than 80% of organizations have explored or piloted tools like ChatGPT, Copilot, and similar assistants, and about 40% report some level of deployment.</p>
<p>But these deployments mostly live at the edge of the business: helping individual employees draft emails, summarize documents or clean up code. Useful? Absolutely. Transformative? Not yet.</p>
<p>When the study turned to structural change across industries (things like new market leaders, AI-native business models, or changes in customer behavior) the picture was stark:only two sectors clearly show signs of AI-driven disruption. Technology and Media &amp; Telecom.</p>
<p>Seven out of the nine industries show lots of pilots, almost no fundamental change.…</p>
<p>Executives feel the disconnect. As one manufacturing COO put it candidly: “The hype says everything has changed. In our operations, we’re just processing contracts a bit faster.”</p>
<p>The divide isn’t about interest or investment; it’s about impact.</p>
<h2 id="why-pilots-stall%3A-the-learning-gap" tabindex="-1">Why Pilots Stall: The Learning Gap</h2>
<p>Stripping the buzzwords away, most of the failures of GenAI in the enterprise come down to one problem: the tools don’t learn.</p>
<p>The research in the report across 52 organizations found that the biggest barriers to scaling AI weren’t regulation, infrastructure, or even talent. They were all symptoms of a deeper learning gap:</p>
<ul>
<li>
<p>Systems do not store feedback.</p>
</li>
<li>
<p>They don’t adapt to messy reality.</p>
</li>
<li>
<p>They can’t evolve with changing workflows.</p>
</li>
</ul>
<p>That’s why generic tools and “LLM wrappers” can tend to do well for ad-hoc work but fall apart in mission-critical workflows. Chat-based tools still expect users to paste the full context every time. Internal tools often ship as rigid, static products that ignore how people actually work.</p>
<p>When asked what holds back the GenAI pilots, concerns about “model quality” showed up high on the list-but not because the underlying models are weak. These same users happily rely on ChatGPT in their personal workflows. But once AI is embedded into enterprise systems, people expect something more: context, continuity, and memory.</p>
<p>In other words, intelligence without learning just isn’t enough.</p>
<p>Nowadays, most of the companies are focused on building their own data with some basic RAG pipelines or out-the-box ready solutions. But these attempts are crushed with the harsh reality: in-context search is not the same as real knowledge, throwing everything into a mixer, blending it together and then passing to the LLM doesn’t work without some extra dedication on building the proper knowledge (in a mathematical sense) base. And these in conjunction with an intelligent retrieval on top are costly and very complicated to develop properly.</p>
<h2 id="the-shadow-ai-economy%3A-what-actually-works" tabindex="-1">The Shadow AI Economy: What Actually Works</h2>
<p>Here’s the paradox: while official AI programs struggle, AI is already changing work—just not in ways IT can see.</p>
<p>The study revealed a burgeoning “shadow AI economy” within organizations:</p>
<ul>
<li>
<p>Only about ~40% of companies report having purchased an official LLM subscription.</p>
</li>
<li>
<p>But employees at more than 90% of the responding companies report using personal tools for work on a regular basis, including ChatGPT and Claude.</p>
</li>
</ul>
<p>Many of these are being used multiple times a day by staff, while corporate AI pilots are stuck in protracted “evaluation phases.”</p>
<p>This represents an uncomfortable truth to leadership, yet is also a powerful signal: employees will adopt AI when it is flexible, responsive, and clearly useful.</p>
<p>They won’t use brittle, overengineered, or badly integrated AI in their workflows, no matter how strategic the initiative appears on the roadmap.</p>
<p>The forward-looking organizations are starting to pay attention to this shadow economy, studying how power users actually get value from AI and using that as a blueprint for official solutions.</p>
<h2 id="builders-vs.-buyers%3A-two-ways-across-the-divide" tabindex="-1">Builders vs. Buyers: Two Ways Across the Divide</h2>
<p>The report highlights another uncomfortable finding: internal builds fail twice as often as external partnerships. Externally developed, learning-capable tools reached deployment roughly 67% of the time in the sample, versus around 33% for in-house builds.</p>
<p>That doesn’t mean “never build.” It means:</p>
<ul>
<li>
<p>Manage AI more like a BPO or consulting engagement than a traditional SaaS rollout.</p>
</li>
<li>
<p>Anticipate a co-evolution process with your vendor, rather than an off-the-shelf and set-and-forget product.</p>
</li>
</ul>
<p>In our experience, organizations that successfully cross the divide from the buyer’s side tend to do three things differently:</p>
<ul>
<li>
<p>They buy like operators, not tourists passing by flashy storefront.</p>
</li>
<li>
<p>They benchmark vendors on business outcomes, not model benchmarks or fancy feature demos. Their questions: What P&amp;L metric will this move? How fast? With what baseline?</p>
</li>
<li>
<p>They empower line managers, not just central AI labs.</p>
</li>
</ul>
<p>The most successful deployments often start with power users who already rely on AI in their daily work. These managers come with concrete use cases and own the rollout, supported by central teams providing governance and guardrails. They demand tools that learn.</p>
<p>The executives consistently identified a short list of must-haves in the interviews: deep understanding of their workflow, minimal disruption to existing tools, clear data boundaries,<br>
and crucially, the ability to improve over time.</p>
<p>From the builder’s side of things, the winning playbook is surprisingly consistent:</p>
<ul>
<li>
<p>Start with narrow, high-value well-specified workflows: for example, contract review, call summarization, repetitive coding tasks.</p>
</li>
<li>
<p>Integrate deeply into the system, rather than trying to replace it.</p>
</li>
<li>
<p>Build ground truth datasets and validation metrics to be able to accurately evaluate results of your non-deterministic AI agents.</p>
</li>
<li>
<p>Build in persistent memory and feedback loops from the first day.</p>
</li>
<li>
<p>Scale from visible, low-risk edge workflows into wider applications.</p>
</li>
</ul>
<p>Those startups doing this well are achieving seven-figure run rates within 6–12 months, not by promising a generic “AI platform,” but by quietly becoming indispensable in one or two workflows first.</p>
<h2 id="the-roi-nobody-brags-about%3A-back-office-wins" tabindex="-1">The ROI Nobody Brags About: Back-Office Wins</h2>
<p>Another pattern in the data is almost comically human: executives tend to put the AI budget where it’s easiest to tell a story.</p>
<p>Asked to hypothetically budget for GenAI, respondents steered about 70% of it toward sales and marketing use cases. Email automation, lead scoring, content generation, campaign orchestration – easy to pitch, easy to measure, easy to talk about in board decks.</p>
<p>But some of the biggest and clearest ROI the study found lives elsewhere:</p>
<ul>
<li>
<p>Automating back-office processes cuts up to $2–10M annually in BPO spend on customer service and document processing.</p>
</li>
<li>
<p>30% reductions in external agency costs related to content and creative work.</p>
</li>
<li>
<p>Unlock significant savings in risk and compliance workflows in financial services.</p>
</li>
</ul>
<p>Interestingly, these gains do not often come from mass layoffs. Organizations that have crossed the GenAI Divide tend instead to:</p>
<ul>
<li>
<p>Reduce outsourced, third-party spend</p>
</li>
<li>
<p>Avoid incremental hiring in certain functions</p>
</li>
<li>
<p>Reassign internal teams to higher-value work rather than cutting them altogether.</p>
</li>
</ul>
<p>Front-office AI gets the limelight, but back-office AI often gets the returns.</p>
<h2 id="what-comes-next%3A-from-agents-to-the-agentic-web" tabindex="-1">What Comes Next: From Agents to the Agentic Web</h2>
<p>The tools of GenAI today still live mostly inside single products: an assistant inside your CRM, a copilot in your office suite, a chatbot on your support site.</p>
<p>The next leap isn’t just “more capable models”, but an Agentic Web: a mesh of interoperating agents able to learn, coordinate, and act across tools, vendors, and even companies. Protocols such as MCP (Model Context Protocol), A2A (Agent-to-Agent), and NANDA form early infrastructure for that world.</p>
<p>In such an environment, AI systems would be able to:</p>
<ul>
<li>
<p>Discover and evaluate vendors independently</p>
</li>
<li>
<p>Spin up dynamic API connections, instead of waiting for hand-coded integrations.</p>
</li>
<li>
<p>Orchestrate multi-step workflows across multiple platforms and organizations</p>
</li>
<li>
<p>Continuously optimize processes based on actual outcomes.</p>
</li>
</ul>
<p>The key implication for enterprises is this: the vendors you train today will shape your flexibility tomorrow.</p>
<p>Once a system has deeply trained on your processes, data, and edge cases, the switching costs become enormous. Procurement leaders in the study estimate that many of these relationships will effectively “lock in” over the next 18-24 months.</p>
<p>That’s why the GenAI Divide is more than a research term; it’s a strategic clock.</p>
<h2 id="a-practical-way-forward" tabindex="-1">A Practical Way Forward</h2>
<p>For those organizations still on the wrong side of the divide, the road ahead is less glamorous than the hype would suggest-but far more achievable:</p>
<ul>
<li>Stop chasing demos.</li>
<li>Focus on workflows where time, cost and error rates are already measured and where AI can plug into existing systems rather than replace them.</li>
<li>Consider AI initiatives to be learning systems, not static products.</li>
<li>Demand persistent memory, feedback loops, and measurable improvement over time.</li>
<li>Start where your people already are. Talk to the “shadow AI” users. Understand what tasks they quietly automate. And turn those hacks into supported, secure, enterprise-grade solutions.</li>
<li>Partner with builders that live in your workflow. Whether you build, buy, or blend both, work with teams that understand your approvals, data flows, and edge cases – not just your industry buzzwords. The GenAI Divide is real, but it’s not permanent.</li>
</ul>
<p>The organizations that cross it first won’t necessarily be the ones with the biggest budgets or flashiest AI labs. They’ll be the ones that treat AI not as a miracle button but as a new kind of infrastructure: embedded, adaptive, and above all, capable of learning.</p>
]]></content:encoded>
            <author>hi+ivan-smetannikov@serokell.co (Ivan Smetannikov)</author>
            <enclosure url="https://serokell.co/files/ak/thumb.akeanmo.normal-Beyond_the_Hype__Crossing_the_GenAI_Divide_in_Real-World_Business.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Design Patterns for Long-Term Memory in LLM-Powered Architectures]]></title>
            <link>https://serokell.co/blog/design-patterns-for-long-term-memory-in-llm-powered-architectures</link>
            <guid isPermaLink="false">https://serokell.co/blog/design-patterns-for-long-term-memory-in-llm-powered-architectures</guid>
            <pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[ The explosive growth of large language models (LLMs) has reshaped the AI landscape. Yet their core design is still fundamentally stateless: a drawback often referred to as “conversational amnesia.” …]]></description>
            <content:encoded><![CDATA[<h1 id="design-patterns-for-long-term-memory-in-llm-powered-architectures" tabindex="-1"><strong>Design Patterns for Long-Term Memory in LLM-Powered Architectures</strong></h1>
<p>The explosive growth of large language models (LLMs) has reshaped the AI landscape. Yet their core design is still fundamentally stateless: a drawback often referred to as “conversational amnesia.” An LLM can only operate within a limited context window and, paradoxically, loses more signal as that window grows, making it unable to reliably carry information forward across extended interactions.</p>
<p>This limitation remains the key blocker to building truly persistent, collaborative, and personalized AI agents that can handle complex, multi-step, long-running workflows. To overcome it, the industry is moving past traditional stateless Retrieval-Augmented Generation (RAG) and toward more advanced architectural patterns purpose-built for long-term memory.</p>
<p>This report outlines and contrasts four leading design philosophies that have emerged:</p>
<ol>
<li><strong>The Operating System Paradigm (MemGPT)</strong>. Treats memory as a managed computational resource, virtualizing LLM context to simulate infinite capacity.</li>
<li><strong>OpenAI memory management.</strong> A product-driven approach where memory enables seamless, persistent personalization across all interactions.</li>
<li><strong>Claude memory management.</strong> Prioritizes user control and strict data isolation, using memory as a project-scoped workspace tool.</li>
<li><strong>AI Toolkits memory management.</strong> Open source provides building blocks for developers to design custom, domain-specific memory systems.</li>
</ol>
<p>Our high-level findings reveal a clear trade-off between automated convenience and explicit control. Emerging systems such as <strong>MemGPT</strong> and modular <strong>agent frameworks</strong> point toward a future of autonomous, self-managing memory.</p>
<hr>
<h2 id="system-i%3A-memgpt-%E2%80%94-the-operating-system-paradigm" tabindex="-1"><strong>System I: MemGPT — The Operating System Paradigm</strong></h2>
<p><strong>MemGPT</strong> represents a fundamental architectural shift: it reframes memory not as a content-management problem but as a <em>resource-management</em> challenge. Inspired directly by computer operating system design, it treats the LLM’s finite context window not as a hard limit but as a form of <em>fast, volatile memory</em> (analogous to RAM), to be intelligently managed alongside a larger, persistent storage layer (analogous to disk).</p>
<p>According to the well-known researcher Andrej Karpathy, a lot of computing concepts carry over in this paradigm. Concepts from computer security carry over, with attacks, defenses and emerging vulnerabilities. E.g. today it orchestrates:</p>
<ul>
<li>Input &amp; Output across modalities (text, audio, vision)</li>
<li>Code interpreter, ability to write and run programs</li>
<li>Browser / internet access</li>
<li>Embeddings database for files and internal memory storage and retrieval</li>
</ul>
<p>According to him, looking at LLMs as chatbots is the same as looking at early computers as calculators. We’re seeing an emergence of a whole new computing paradigm. and it is very early.</p>
<h3 id="core-architecture%3A-virtual-context-management" tabindex="-1"><strong>Core Architecture: Virtual Context Management</strong></h3>
<p>At its core, MemGPT features a hierarchical memory architecture closely mirroring that of a traditional OS:</p>
<ul>
<li>
<p><strong>Primary Context (RAM)</strong> — The fixed-size prompt that the LLM can directly “see” during inference.<br>
It consists of three partitions:</p>
<ol>
<li>
<p><strong>Static system prompt</strong>, containing base instructions and function schemas.</p>
</li>
<li>
<p><strong>Dynamic working context</strong>, serving as a scratchpad for reasoning steps and intermediate results.</p>
</li>
<li>
<p><strong>FIFO message buffer</strong>, holding the most recent conversational turns.</p>
</li>
</ol>
</li>
<li>
<p><strong>External Context (Disk Storage)</strong> — An effectively infinite, out-of-context layer inaccessible to the model without explicit retrieval.<br>
It includes:</p>
<ol>
<li>
<p><strong>Recall Storage</strong>, a searchable document or log database containing the full historical record of interactions for literal recall.</p>
</li>
<li>
<p><strong>Archival Storage</strong>, a long-term, vector-based memory for large documents and abstracted knowledge retrievable via semantic search.</p>
</li>
</ol>
</li>
</ul>
<p><img src="https://serokell.co/files/a8/a84bwbk.Untitled_document_Img0.png" alt="a84bwbk.Untitled_document_Img0.png"></p>
<p>Information flow between these tiers is governed by a strict <em>paging mechanism</em>. The LLM “processor” works only within the primary context. To access external data, it must autonomously issue explicit function calls (e.g., <code>conversation_search</code>, <code>archival_memory_search</code>).</p>
<p>Results from these calls are then <em>paged in</em>, replacing less relevant segments in the FIFO queue, and creating the illusion of an unbounded context window while remaining within the physical token limits of the underlying model.</p>
<h3 id="memory-formation%3A-the-self-managed-write-back-cycle" tabindex="-1"><strong>Memory Formation: The Self-Managed Write-Back Cycle</strong></h3>
<p>Persisting information into long-term memory occurs through an <em>event-driven write-back cycle</em>, analogous to an OS interrupt.</p>
<p>The process is triggered by <strong>memory pressure</strong>: when token usage in the primary context approaches a defined threshold (e.g., 70% capacity), the system inserts an internal alert.<br>
Upon receiving this signal, the LLM halts its current reasoning, reviews its working memory, determines which content is least critical, summarizes it, and writes it to the appropriate external tier.</p>
<p>Crucially, <strong>the LLM itself manages this cycle</strong>—autonomously deciding what to keep, what to discard, and where to store it.<br>
This self-reflective capability allows for dynamic memory correction (e.g., overwriting outdated facts) and embodies a primitive form of cognitive self-regulation.</p>
<h3 id="tooling-and-technology-stack" tabindex="-1"><strong>Tooling and Technology Stack</strong></h3>
<p>Typical implementations leverage the following stack:</p>
<ul>
<li>
<p><strong>Core Framework:</strong> The original <em>MemGPT</em> open-source Python framework, now evolved into <strong>Letta</strong>.</p>
</li>
<li>
<p><strong>LLM Compatibility:</strong> Model-agnostic but optimized for function-calling models such as OpenAI’s GPT-4 or GPT-3.5.</p>
</li>
<li>
<p><strong>Vector Databases:</strong> Used for archival semantic search; common options include Chroma, LanceDB, or pgvector.</p>
</li>
<li>
<p><strong>Persistent Storage:</strong> Recall layers often use lightweight databases or file systems for event logs and message histories.</p>
</li>
</ul>
<p><strong>Strengths:</strong><br>
Elegant abstraction of the finite-context problem; creates the illusion of infinite memory via virtualization.</p>
<p><strong>Limitations:</strong><br>
All reasoning and memory management are handled by a single agent, consuming valuable cognitive bandwidth.<br>
Because stored data is unstructured, performing complex relational queries (e.g., “Which decisions were influenced by facts from source X?”) is nearly impossible without heavy post-processing—precisely what MaaS aims to solve.</p>
<p>In short, MemGPT’s brilliance lies in its autonomy—but that autonomy comes at a cost. Every cycle spent on memory logistics is a cycle not spent on task reasoning.</p>
<hr>
<h2 id="system-ii%3A-openai-memory-management" tabindex="-1"><strong>System II: OpenAI Memory Management</strong></h2>
<p>OpenAI’s memory for ChatGPT represents a <em>product-first</em> architecture designed to deliver a seamless, deeply personalized experience.<br>
Unlike other models, it implements a <em>global</em>, user-centric memory that persists across all conversations, making the assistant continuously aware and contextually intelligent with minimal user effort.</p>
<h3 id="core-architecture%3A-hybrid-fact-%2B-semantic-storage" tabindex="-1"><strong>Core Architecture: Hybrid Fact + Semantic Storage</strong></h3>
<p>The system combines two complementary memory layers:</p>
<ol>
<li>
<p><strong>Saved Memories</strong>. While we do not exactly know how it is organized, it can be viewed as a page or two of different facts collected together from all of your chats. LLM decides automatically which of the data you’ve provided via your current conversation is added there. These can be explicitly provided (“Remember that I live in San Francisco”) or automatically classified by the model as potentially useful. Each session begins with this document prepended to the LLM prompt, ensuring continuity. We assume that they also might use some simple key-value database to manage it more accurately.</p>
</li>
<li>
<p><strong>Chat History Reference</strong>. A large-scale RAG-style retrieval layer that semantically searches across all previous user interactions to find relevant context fragments for the current query. According to the system’s behaviour we assume that this search is working on the whole text, not on the summaries of these chats.</p>
</li>
</ol>
<p>During response generation, both layers are queried in parallel. Memories are added to the beginning of the conversation. Semantically matched chat snippets are combined into the model’s context according to the concrete request, yielding personalized, contextually aware outputs.</p>
<h3 id="memory-formation" tabindex="-1"><strong>Memory Formation</strong></h3>
<p>The write-back cycle operates in two modes:</p>
<ul>
<li>
<p><strong>Explicit Commands.</strong> The user directly instructs the model to remember or forget something.</p>
</li>
<li>
<p><strong>Automatic Extraction.</strong> Background classifiers continuously scan conversations to identify recurring or salient information (e.g., profession, tone preferences). Extracted facts are either suggested to or silently added into the saved memory layer.</p>
</li>
</ul>
<p><strong>User oversight remains central.</strong> Through the ChatGPT settings interface, users can view, edit, or delete any stored memory, ensuring transparency, privacy, and control.</p>
<h3 id="technology-stack-(presumably)" tabindex="-1"><strong>Technology Stack (Presumably)</strong></h3>
<p>While proprietary, the architecture likely includes:</p>
<ul>
<li>
<p>A high-performance vector store for semantic retrieval over historical chats.</p>
</li>
<li>
<p>A scalable key-value or document database for structured “saved memories.”</p>
</li>
<li>
<p>A background extraction pipeline leveraging a classification model.</p>
</li>
</ul>
<p><strong>Strengths:</strong><br>
Delivers effortless, “magical” personalization at global scale, which is perfect for user application.</p>
<p><strong>Limitations:</strong><br>
Global scope makes it unsuitable for enterprise or professional contexts. The risk of <em>context leakage</em> (where information from one client or topic influences another) is inherent to its design. Also it is not supporting multi-user usage in the same scope, at least for now.</p>
<p>In essence, OpenAI’s architecture mirrors its business strategy: prioritize simplicity and personalization for a mass audience over data compartmentalization or symbolic structure. Its strength lies in accessibility and its limitation lies in lack of user control.</p>
<h2 id="system-iii%3A-claude-memory-management" tabindex="-1"><strong>System III: Claude Memory Management</strong></h2>
<p><strong>Claude’s</strong> approach stands as a philosophical counterpoint to OpenAI’s global personalization. Anthropic emphasizes <em>user control</em>, <em>explicit activation</em>, and <em>strict data compartmentalization</em>, producing a memory system that is less automated but more predictable: well-suited to professional workflows where data separation is non-negotiable.</p>
<h3 id="core-architecture%3A-project-summaries-and-file-scoped-context" tabindex="-1"><strong>Core Architecture: Project Summaries and File-Scoped Context</strong></h3>
<p>Claude’s memory combines official features with strong, community-driven patterns:</p>
<ul>
<li>
<p><strong>Project Memory (Team/Enterprise).</strong> Users create distinct <em>projects</em>, each with its own editable memory summary of key facts, instructions, and context. That summary is automatically injected into prompts for all chats within the same project, enforcing hard boundaries – nothing from <em>Project A</em> can bleed into <em>Project B</em>.</p>
</li>
<li>
<p><strong>Community Patterns in <code>CLAUDE.md</code>.</strong> In developer workflows, teams often include a <code>CLAUDE.md</code> file at the repo root. When interacting with Claude in that working directory, the file is read and included in context. This acts like “context injection,” not dynamic RAG: the whole file is loaded, enabling versioned, reviewable, and Git-managed context (architecture principles, coding standards, API specs) that sits alongside the codebase.</p>
</li>
<li>
<p><strong>On-Demand Retrieval Tools.</strong> When explicitly asked to “recall” something from the past, Claude appears to use internal tools (e.g., <code>conversation_search</code>) confined to the <em>current project</em>. It doesn’t rely on a global index, reinforcing compartmentalization.</p>
</li>
</ul>
<h3 id="memory-formation%3A-mostly-curated-by-the-user" tabindex="-1"><strong>Memory Formation: Mostly Curated by the User</strong></h3>
<p>In contrast to OpenAI’s automation, Claude’s write-back cycle is largely <em>explicit</em>:</p>
<ul>
<li>
<p>Project memory is updated because a user <em>asks</em> for it to be updated.</p>
</li>
<li>
<p>The <code>CLAUDE.md</code> pattern is edited by humans via normal version control.</p>
</li>
<li>
<p>Memory is not “always on”; users often prompt Claude to look back, which in turn activates the project-limited search tools.</p>
</li>
<li>
<p>The search implementation has two key elements:</p>
<ul>
<li>Conversation search. Makes keyword and topic-based searches across the entire conversation history.</li>
<li>Recent chats. Provides time-based access to the conversation history with customizable sort chronological order and optional pagination using ‘before’ and ‘after’ datetime filters.</li>
</ul>
</li>
</ul>
<h3 id="technology-stack" tabindex="-1"><strong>Technology Stack</strong></h3>
<ul>
<li>
<p><strong>Anthropic Platform:</strong> Project memory is a first-class feature in Claude’s web app/API for Team/Enterprise plans.</p>
</li>
<li>
<p><strong>Files + Version Control:</strong> The <code>CLAUDE.md</code> pattern relies only on standard files, editors, and Git.</p>
</li>
<li>
<p><strong>3rd-Party Connectors:</strong> An emerging ecosystem (e.g., CLI tools) exposes long-term or local stores via Claude tool integrations.</p>
</li>
</ul>
<p><strong>Strengths:</strong><br>
Advanced control, predictability, and transparency. Ideal for client work and regulated environments where data isolation is paramount.</p>
<p><strong>Limitations:</strong><br>
High cognitive load and limited scalability. Memory grows only as fast as users curate it.<br>
Large, monolithic summaries risk the loss of information.</p>
<hr>
<h2 id="system-iv%3A-ai-toolkits-memory-management" tabindex="-1"><strong>System IV: AI Toolkits Memory Management</strong></h2>
<p><strong>LangChain</strong> and <strong>Microsoft Autogen</strong> aren’t memory products; they’re <em>frameworks</em> that give you the primitives to assemble sophisticated memory architectures. The payoff is fine-grained control, but the cost is engineering effort.</p>
<h3 id="core-architecture%3A-composable-and-protocol-oriented" tabindex="-1"><strong>Core Architecture: Composable and Protocol-Oriented</strong></h3>
<ul>
<li>
<p><strong>LangChain Memory Modules:</strong></p>
<ul>
<li>
<p><em>Buffer Memory and Window Memory</em> (short-term chat windows),</p>
</li>
<li>
<p><em>Summary Memory</em> (periodic LLM summaries to save tokens),</p>
</li>
<li>
<p><em>Entity Memory</em> (structured capture of people/orgs/concepts),</p>
</li>
<li>
<p><em>Knowledge-Graph Memory</em> that builds nodes/edges on the fly for relational querying.</p>
</li>
</ul>
</li>
<li>
<p><strong>LangGraph for Stateful Agents.</strong> A graph-based orchestration layer for building cyclic, multi-agent workflows where memory is an explicit part of the agent’s state persisted across steps and sessions.</p>
</li>
<li>
<p><strong>Autogen Memory Protocol.</strong> A generalized Memory interface with a RAG-centric pattern: agents query external memory stores to enrich their context. Community integrations cover vector DBs and third-party memory services.</p>
</li>
</ul>
<h3 id="memory-formation%3A-your-design" tabindex="-1"><strong>Memory Formation: Your Design</strong></h3>
<p>These frameworks don’t ship with an “always-on” write-back loop. You design it yourself. For example:</p>
<ol>
<li>The agent completes a step.</li>
<li>A “memory extraction” tool (often LLM-powered) distills facts, entities, or relationships.</li>
<li>A “memory store” tool writes structured results to the chosen backend (vector store, graph DB, KV/Doc store).</li>
</ol>
<h3 id="technology-stack-1" tabindex="-1"><strong>Technology Stack</strong></h3>
<ul>
<li><strong>Languages:</strong> Python and JavaScript/TypeScript.</li>
<li><strong>Orchestration:</strong> LangChain, LangGraph, Autogen</li>
<li><strong>Pluggable Datastores:</strong>
<ul>
<li><em>Vector stores:</em> Pinecone, Chroma, Weaviate, LanceDB.</li>
<li><em>Graph DBs:</em> Neo4j, Memgraph, Kùzu.</li>
<li><em>KV/Document stores:</em> Redis, MongoDB.</li>
</ul>
</li>
</ul>
<p>Basically anything you want as they are both open-sourced.</p>
<p><strong>Strengths:</strong><br>
Unmatched flexibility. You can encode the exact memory structure, DB tech, and agent logic your domain needs, often surpassing off-the-shelf systems with the precise tinkering.</p>
<p><strong>Limitations:</strong><br>
Complex and expensive to build and maintain. Reliability, scale, consistency, and data governance are your responsibility.</p>
<p><strong>Industry Trajectory:</strong><br>
Early memory systems leaned on raw text + vector search. That hits a ceiling for relational questions (“Who’s working with Alice on currently blocked projects?”). Later we introduced entities and relationships, <strong>knowledge graphs</strong>. Patterns like LangChain’s KG memory and growing Graph DB integrations signal a shift from mere similarity search toward <em>relational reasoning</em>, the most plausible path to collaborative, “systems-level” agents.</p>
<hr>
<h2 id="final-thoughts-on-the-current-state" tabindex="-1"><strong>Final thoughts on the current state</strong></h2>
<p>Basically every approach has its strengths and weaknesses, to quickly summarize:</p>
<ul>
<li><strong>OpenAI.</strong> Frictionless, “magical” continuity for individuals: great for consumers but risky for enterprise separation.</li>
<li><strong>Claude.</strong> Strong isolation and user control: great for client work and regulated contexts but manual effort required.</li>
<li><strong>MemGPT.</strong> Autonomous context virtualization: near-infinite memory feel but single-agent overhead and unstructured storage limit relational queries.</li>
<li><strong>Toolkits.</strong> Maximum customizability: best path to tailor-made, domain-specific systems but highest build complexity.</li>
</ul>
<p>While there is no universally accepted approach to handle AI agents memory management right now, we can foresee some strong emerging trends and patterns that will result in more stable, reliable, safe and interpretable AI systems in the near future. For example, modern systems went from direct context sharing into some RAG capabilities and now are moving into <strong>autonomous memory orchestration</strong> with LLMs using tools provided by developers. In order to handle the logic and provide additional guardrails industry is shifting from unstructured snippets and restricting prompts with cornercases into direct <strong>knowledge graph usage</strong>. And nowadays, in order to prevent context overflow more systems are moving from single agent calls into <strong>multi-agent pipelines</strong>. These pipelines might accumulate a lot of errors due to the snowball effect of inaccurate agents being called on each step, which also will be overcome with knowledge graphs, shared memory pipelines and its <strong>strict typing with audition capabilities.</strong></p>
]]></content:encoded>
            <author>hi+ivan-smetannikov@serokell.co (Ivan Smetannikov)</author>
            <enclosure url="https://serokell.co/files/ah/thumb.ahdfjji.normal-Design_Patterns_for_Long-Term_Memory_in_LLM-Powered_Architectures.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[The Real Limits of AI Agents in 2025]]></title>
            <link>https://serokell.co/blog/the-real-limits-of-ai-agents-in-2025</link>
            <guid isPermaLink="false">https://serokell.co/blog/the-real-limits-of-ai-agents-in-2025</guid>
            <pubDate>Sun, 02 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[TL;DR: Everyone says 2025 is the year of autonomous AI agents. We’ve built a lot of them in production, and that’s exactly why we think most of the current hype just doesn’t add up. In this post, we'll break down the most common misconceptions, talk about what actually works in the real world, and explain why the math and economics behind the hype don’t hold up yet.]]></description>
            <content:encoded><![CDATA[<p>TL;DR: Everyone says 2025 is the year of autonomous AI agents. We’ve built a lot of them in production, and that’s exactly why we think most of the current hype just doesn’t add up. In this post, we’ll break down the most common misconceptions, talk about what actually works in the real world, and explain why the math and economics behind the hype don’t hold up yet.</p>
<h3 id="rumors-and-speculations-breakdown" tabindex="-1"><strong>Rumors and Speculations Breakdown</strong></h3>
<p><strong>“Autonomous AI agents will replace traditional workflows in 2025!”</strong><br>
Not really. The idea of fully autonomous multi-step agents sounds great, but in practice it falls apart under simple math. The issue isn’t intelligence or prompt quality, it’s compounded error rates. Even small per-step mistakes grow exponentially over time, which makes true end-to-end autonomy impossible at scale.</p>
<p><strong>“Conversational agents are the next big thing!”</strong><br>
Maybe, but not in the way most people think. Long-context agents suffer from quadratic token costs. Every new message has to reprocess the entire conversation, and that makes long sessions ridiculously expensive.</p>
<p><strong>“We just need better APIs and the agents will figure it out!”</strong><br>
Nope. The real bottleneck isn’t model capability, it’s bad tool design. Most “AI agents” today fail because the tools they use don’t give them structured feedback. The AI doesn’t need human-style interfaces, it needs clean, machine-readable signals that help it reason about what just happened.</p>
<h3 id="engineering-reality-breakdown" tabindex="-1"><strong>Engineering Reality Breakdown</strong></h3>
<p>Let’s move from the hype to the practical side: what actually happens when you build and ship AI agents in production.</p>
<h4 id="1.-the-mathematics-behind-failure" tabindex="-1"><strong>1. The Mathematics Behind Failure</strong></h4>
<p>Error compounding quietly kills multi-step autonomy.<br>
Say your model performs each step with 95% accuracy (which is already optimistic). Here’s what happens:</p>
<ul>
<li>
<p>5 steps → 77% success</p>
</li>
<li>
<p>10 steps → 59% success</p>
</li>
<li>
<p>20 steps → 36% success</p>
</li>
</ul>
<p>Real production systems often need 99.9% reliability. Even if you somehow reach 99% per step, you still only get about 82% success across 20 steps. That’s not a prompt issue, that’s just math.</p>
<p>On the contrary, our DevOps agent works precisely because it’s not truly autonomous. It runs 3–5 well-defined operations with rollback points and optional human confirmations. Each step is verifiable, and errors don’t pile up. The “autonomy” part is an illusion built on careful architecture.</p>
<h4 id="2.-token-economics-nobody-mentions" tabindex="-1"><strong>2. Token Economics Nobody Mentions</strong></h4>
<p>There’s another uncomfortable reality: conversational agents are usually too expensive to scale.<br>
Every new exchange requires processing the full conversation history, so token usage grows quadratically.</p>
<p>When we built a conversational database agent, the first few queries were cheap. By the 50th turn, each response can be costing several dollars, more than the value of the query itself. That doesn’t work in production.</p>
<p>That’s why stateless, single-turn agents are often more practical. Our function generator, for example, does one thing: it takes a description, produces a function, and stops there. No memory management, no exploding costs, just fast, cheap, and reliable execution. Ideally you want to validate and store intermediate results in conventional databases and at least try to guardrail them into being deterministic.</p>
<h4 id="3.-the-tool-design-wall" tabindex="-1"><strong>3. The Tool Design Wall</strong></h4>
<p>Even if you solve the math and the cost, there’s another wall waiting: tool engineering. LLMs are now quite good at calling tools, but the real challenge is designing tools that talk back in a way the AI can understand.</p>
<p>You need to think carefully about:</p>
<ul>
<li>
<p>How to report partial successes</p>
</li>
<li>
<p>How to summarize large outputs without burning context</p>
</li>
<li>
<p>How to recover when a tool fails</p>
</li>
<li>
<p>How to handle dependencies between tools</p>
</li>
</ul>
<p>Our database agent works well only because each tool returns structured, meaningful feedback, not just raw API dumps. That took weeks to get right. The truth is, the AI handles maybe 30% of the logic. The other 70% is the surrounding engineering: feedback design, context management, AI guardrails, error handling, and recovery mechanisms. All these mechanisms are trying to fit non-deterministic and unpredictable AI behavior into the strict frame reducing error rate drastically.</p>
<h3 id="integration-breakdown" tabindex="-1"><strong>Integration Breakdown</strong></h3>
<p>And even if you fix everything else, you still need to connect your agent to real systems, and real systems are messy.<br>
Enterprise software isn’t a collection of clean APIs. It’s full of quirks, legacy components, unpredictable rate limits, and compliance rules that change overnight.</p>
<p>Our production database agent doesn’t just “run queries on its own.” It manages transaction safety, connection pools, audit logs, and rollback logic — all the boring, reliable stuff you need to make things actually work. Integration is where most AI agents fail quietly.</p>
<h3 id="what-actually-works" tabindex="-1"><strong>What Actually Works</strong></h3>
<p>After building several different agent systems, a clear pattern has emerged. The ones that work all look surprisingly similar:</p>
<ul>
<li>UI generation agents succeed because humans review everything before deployment.</li>
<li>Database agents work because potentially destructive actions require confirmation.</li>
<li>Function generators work because they’re stateless and self-contained.</li>
<li>DevOps agents work because they output infrastructure-as-code that humans can review and roll back.</li>
<li>CI/CD agents work because the pipeline enforces strict success and rollback criteria.</li>
</ul>
<p>And all these agents work only if you have clear and straightforward guidelines on a granular task you want them to perform. Just as you would explain something to a real person who never did it: with all the caveats and potential problems.</p>
<p>The pattern is simple: AI handles complexity, humans keep control, and traditional software ensures reliability.</p>
<h3 id="predictions-for-the-end-of-2025" tabindex="-1"><strong>Predictions for the end of 2025</strong></h3>
<p>Here’s how we think 2025 will play out:</p>
<ul>
<li>Startups chasing “fully autonomous agents” will hit a hard wall with cost and reliability. Few-step demos don’t survive real 20-step workflows. Real data and tools accessed via magic of MCP but without clear guidelines will not result in high accuracy even on simple few-steps pipelines.</li>
<li>Big enterprise tools that just slap “AI agent” onto their existing products will stall because their integrations can’t handle the real world.</li>
<li>The real winners will build focused, domain-specific assistants that use AI where it helps most, but still rely on humans or deterministic systems for critical control points and general AI agents guidance.</li>
</ul>
<p>Eventually, people will realize the difference between AI that demos well and AI that actually ships. It’s going to be an expensive lesson.</p>
<h3 id="building-the-right-way" tabindex="-1"><strong>Building the Right Way</strong></h3>
<p>If you’re building AI agents this year, start with these principles:</p>
<ol>
<li><strong>Define a clear problem you want to solve or automate.</strong> AI is not a magic box, its usage stays on the same principles as a classical software development. The only difference is that it can handle unstructured data with much less development effort and it has non-deterministic output.</li>
<li><strong>Split the problem into verifiable pieces where possible.</strong> Instead of building a single complex agent make several small ones. If needed, make extra “manager” or “intermediate” agents that will aggregate results of several other agents.</li>
<li><strong>Provide clean instructions.</strong> Each set of instructions should be clear and straightforward. Try to cover all cornercases, but not overthink it: if instructions become too complicated or too long then return to step 2.</li>
<li><strong>Define clear boundaries.</strong> Know exactly what your agent can do and when it should stop. Be ten times more careful with agents that provide you with data and not just summarize and build reports. Do not give direct write access to the agent if it works with sensitive information.</li>
<li><strong>Design for failure.</strong> Assume 20–40% of operations will go wrong. Have rollback plans. Always have a “ground truth” source of information which was not touched by AI at all. Have the full log of what was done in the system with an emergency script that can use your “ground truth” data to rebuild everything if needed.</li>
<li><strong>Mind the economics.</strong> Measure token costs and scale realistically. Stateless often beats stateful. Cache agents responses if needed, especially if their job was to generate some intermediate data.</li>
<li><strong>Prioritize reliability over autonomy.</strong> People trust consistent tools more than “magical” ones. Never deploy a new agent to a wide mass of people if you haven’t proved it to be effective. Use some beta-testers with expertise relevant to the application area to tune it up.</li>
<li><strong>Use AI where it shines.</strong> Let it handle reasoning, intent, and generation. Give it unstructured data as input to work with. Leave data processing, execution and state to proven software patterns.</li>
</ol>
<h3 id="outro" tabindex="-1"><strong>Outro</strong></h3>
<p>The agent revolution is real, but it’s not going to look like the hype suggests. The winning systems won’t be fully autonomous. They’ll be thoughtful combinations of AI reasoning, human judgment, and traditional engineering discipline.</p>
<p>We are not betting against AI. We are betting against the current obsession with its overpromising use. The real breakthroughs will come from teams who understand the limits, respect the math, and build around reality instead of wishful thinking.</p>
<p><strong>Far-sight outlook:</strong><br>
Still, it’s worth thinking about where this all leads. Just like deep learning eventually replaced handcrafted pipelines with end-to-end systems, agents will likely follow the same path.</p>
<p>Over time, meta-learning and new reinforcement-learning methods — ones that don’t even exist yet — will let models learn not just <em>tasks</em>, but <em>how to learn</em>.</p>
<p>They’ll be able to adapt to feedback, handle rare edge cases, and self-correct in ways we currently have to hardcode. When that happens, the rigid guardrails we depend on today will turn into adaptive self-tuning mechanisms, and we’ll finally reach the true end-to-end agent era where you just add extra input as the system goes and it autocorrects itself accordingly without any additional inputs from your side.</p>
]]></content:encoded>
            <author>hi+ivan-smetannikov@serokell.co (Ivan Smetannikov)</author>
            <enclosure url="https://serokell.co/files/ao/thumb.aod9jco.normal-The_Real_Limits_of_AI_Agents_in_2025.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Reviving an Old iMac with NixOS]]></title>
            <link>https://serokell.co/blog/reviving-an-old-imac-with-nixos</link>
            <guid isPermaLink="false">https://serokell.co/blog/reviving-an-old-imac-with-nixos</guid>
            <pubDate>Sun, 14 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[ People have been using computers for decades. Information technology advances by leaps and bounds. As a result, yesterday’s new, powerful machines quickly become today’s obsolete hardware, gatherin…]]></description>
            <content:encoded><![CDATA[<p>People have been using computers for decades. Information technology advances by leaps and bounds. As a result, yesterday’s new, powerful machines quickly become today’s obsolete hardware, gathering dust on shelves and in closets.</p>
<p>However, these old computers can still be useful for low-resource tasks, such as working with documents, surfing the web, and watching videos. In general, the obsolete hardware is supported by very old software — operating systems and applications — which is vulnerable and unsafe to use. New software cannot be installed or runs too slowly due to a slow CPU and a lack of memory and disk space.</p>
<p>I personally have two examples of such obsolete computers: an Acer Extensa 5220 laptop and an 18-year-old iMac. The iMac has a large display, and it is a shame that I cannot use it safely since its hardware is supported by the very old MacOS Lion. I tried to install several lightweight Linux distributions on it but failed because of:</p>
<ol>
<li>It was not easy to get the Wi-Fi card to work properly.</li>
<li>The software was not responsive.</li>
</ol>
<p>That is why I decided to experiment with NixOS on my old computers, since I know that NixOS allows fine-tuning of the operating system and other software.</p>
<h2 id="goals" tabindex="-1">Goals</h2>
<ol>
<li>Get the old computer working.</li>
<li>Use modern and secure software with available security and feature updates.</li>
<li>Ensure a pleasant and responsive user experience.</li>
</ol>
<h2 id="breathing-new-life-into-the-old-imac" tabindex="-1">Breathing new life into the old iMac</h2>
<p>Generally, the following steps are suitable for any low-spec computer. However, we will focus more on the iMac, as it is a bit more challenging and requires more effort.</p>
<p>We have an iMac made in 2007:</p>
<ul>
<li>CPU: Core2 Duo 2.4Ghz</li>
<li>RAM: 2GB</li>
<li>SSD: 120GB (upgraded some time ago from HDD)</li>
<li>Wi-Fi, Bluetooth</li>
</ul>
<h3 id="problems-we-have-to-solve%3A" tabindex="-1">Problems we have to solve:</h3>
<ol>
<li>
<p>First of all, we need to remove the thick layer of dust that has collected on it.</p>
</li>
<li>
<p>The computer’s hardware is very limited, so we cannot use the graphical installer.</p>
</li>
</ol>
<p>Furthermore, Apple computers have a Broadcom Wi-Fi card that requires a proprietary driver, which is not included in the default NixOS installation image. Of course, it’s possible to connect to the network using an Ethernet cable or tethering, but I don’t have a suitable cable and don’t want to use my mobile data.</p>
<p>That is why we are going to create a custom minimal ISO image with Broadcom Wi-Fi support to be able to install NixOS conveniently.</p>
<ol start="3">
<li>We are going to create a NixOS configuration that is sufficient to meet the requirements of Goal 3.</li>
</ol>
<h3 id="creating-custom-minimal-installation-image" tabindex="-1">Creating custom minimal installation image</h3>
<p>Note: This step is required only for Apple computers and should be skiped for all other hardware. Simply download the minimal installation image from the NixOS website.</p>
<p>Prerequisites: Nix should be installed on the computer which you are going to use for creating the installation image.</p>
<p>Create a working directory, say <code>custom-nixos-iso</code>.</p>
<h4 id="creating-image-with-nix-flakes" tabindex="-1">Creating image with nix flakes</h4>
<p>Create <code>flake.nix</code> file with the following content:</p>
<pre><code class="hljs language-nix">{
  <span class="hljs-attr">description</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;Custom NixOS ISO with Broadcom&quot;</span>;

  <span class="hljs-attr">inputs.nixpkgs.url</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;github:NixOS/nixpkgs/nixos-25.05&quot;</span>;

  <span class="hljs-attr">outputs</span> <span class="hljs-operator">=</span> { self, nixpkgs }:
    <span class="hljs-keyword">let</span>
      <span class="hljs-attr">system</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;x86_64-linux&quot;</span>;
    <span class="hljs-keyword">in</span> {
      <span class="hljs-attr">nixosConfigurations.install-iso</span> <span class="hljs-operator">=</span> nixpkgs.lib.nixosSystem {
        <span class="hljs-keyword">inherit</span> system;
        <span class="hljs-attr">modules</span> <span class="hljs-operator">=</span> [
          ({ config, pkgs, ... }: {
            <span class="hljs-attr">imports</span> <span class="hljs-operator">=</span> [
              <span class="hljs-string">&quot;<span class="hljs-subst">${nixpkgs}</span>/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix&quot;</span>
            ];
            <span class="hljs-attr">hardware.enableRedistributableFirmware</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
            <span class="hljs-comment"># boot.extraModulePackages = [ config.boot.kernelPackages.broadcom_sta ];</span>
            <span class="hljs-attr">boot.kernelModules</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;b43&quot;</span> ];
            <span class="hljs-attr">boot.blacklistedKernelModules</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;wl&quot;</span> ];
            <span class="hljs-attr">networking.enableB43Firmware</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
          })
        ];
      };
    };
}
</code></pre>
<p>After that run in the working directory the following command:</p>
<pre><code class="hljs language-shell">nix build .#nixosConfigurations.install-iso.config.system.build.isoImage
</code></pre>
<p>The image will be placed in the <code>result</code> directory.</p>
<h4 id="creating-image-without-flakes" tabindex="-1">Creating image without flakes</h4>
<p>If you don’t want to use flake you can create installation image with ordinary nix. Create <code>iso.nix</code> file with the following content:</p>
<pre><code class="hljs language-nix">{ config, pkgs, ... }:

{
  <span class="hljs-attr">imports</span> <span class="hljs-operator">=</span> [
    <span class="hljs-operator">&lt;</span>nixpkgs<span class="hljs-operator">/</span>nixos<span class="hljs-operator">/</span>modules<span class="hljs-operator">/</span>installer<span class="hljs-operator">/</span>cd-dvd<span class="hljs-operator">/</span>installation-cd-minimal.nix<span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>nixpkgs<span class="hljs-operator">/</span>nixos<span class="hljs-operator">/</span>modules<span class="hljs-operator">/</span>installer<span class="hljs-operator">/</span>cd-dvd<span class="hljs-operator">/</span>channel.nix<span class="hljs-operator">&gt;</span>
  ];

  <span class="hljs-attr">hardware.enableRedistributableFirmware</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
  <span class="hljs-attr">boot.kernelModules</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;b43&quot;</span> ];
  <span class="hljs-attr">networking.enableB43Firmware</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
  <span class="hljs-comment"># boot.extraModulePackages = [ config.boot.kernelPackages.broadcom_sta ];</span>
  <span class="hljs-attr">boot.blacklistedKernelModules</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;wl&quot;</span> ];
}
</code></pre>
<p>Run in the working directory:</p>
<pre><code class="hljs language-shell">nix-build &#x27;&lt;nixpkgs/nixos&gt;&#x27; -A config.system.build.isoImage -I nixpkgs=channel:nixos-25.05 -I nixos-config=iso.nix
</code></pre>
<h4 id="notes-on-selecting-wi-fi-drivers" tabindex="-1">Notes on selecting Wi-Fi drivers</h4>
<p>Actually, there are only two options:</p>
<ol>
<li>
<p>The original proprietary driver, <code>broadcom_sta</code>, which provides the kernel module <code>wl</code>.</p>
</li>
<li>
<p>The free driver <code>b43</code>, which requires redistributable proprietary firmware.</p>
</li>
</ol>
<p>The best driver is the one that works for you. My experience shows that both can get the Wi-Fi card working. However, <code>broadcom_sta</code> is vulnerable and, in my case, it does not work correctly and creates an incorrect ARP configuration. This means that although the Wi-Fi card obtains an IP address, it does not have access to the gateway. That is why I have enabled <code>b43</code> in my Nix configurations.</p>
<p>If you want to try <code>broadcom_sta</code>, you need to:</p>
<ul>
<li>Uncomment the line that enables this driver</li>
<li>Activate the kernel module wl.</li>
<li>Disable the B43Firmware option.</li>
<li>Blacklist the b43 kernel module.</li>
</ul>
<h3 id="write-the-image-to-a-usb-stick" tabindex="-1">Write the image to a USB stick</h3>
<p>You can use any suitable software for this. However, note that Ventoy should not be used for installing NixOS on Apple computers.</p>
<h3 id="booting-from-the-usb-stick" tabindex="-1">Booting from the USB stick</h3>
<p>When booting your old computer, you must select the USB stick as the boot device. On Apple computers, this is done by pressing the Option key immediately after powering it on.</p>
<h3 id="setting-up-the-internet-connection" tabindex="-1">Setting up the Internet connection</h3>
<p>Before you start installation you should make sure that you have working Internet connection.</p>
<p>First, run command</p>
<pre><code class="hljs language-shell">ip link
</code></pre>
<p>and check if you can see your Wi-Fi card in the output. In my case, it’s the <code>wlan0</code> interface. If you don’t see a Wi-Fi card, it means the driver does not support your hardware. Make sure the driver is loaded (<code>lsmod</code>) and try setting up an alternative driver when creating the installation image.</p>
<p>If Wi-Fi driver works and recognizes your hardware, configure and run your Wi-Fi connection.</p>
<pre><code class="hljs language-shell">wpa_passphrase YOUR_SSID &gt; wpa.conf
sudo wpa_supplicant -B -i wlan0 -c wpa.conf
</code></pre>
<p>Please adjust values for your Wi-Fi network <code>SSID</code> and network interface name to match your setup.</p>
<p>Normally, the <code>dhcpcd</code> daemon is running after boot, so you should get a working Internet connection shortly. If not, make sure you have entered correct values for the <code>SSID</code> and password. To check your connection run the following commands:</p>
<pre><code class="hljs language-shell">ip addr # check that you got an IP address
ping 8.8.8.8 # check if the Internet is accessible
</code></pre>
<h3 id="installing-nixos" tabindex="-1">Installing NixOS</h3>
<h4 id="create-and-mount-disk-partitions" tabindex="-1">Create and mount disk partitions</h4>
<p>Notes:</p>
<ol>
<li>This guide assumes your disk has no existing partitions. If it does, ensure you have backed up any important data and remove all partitions before proceeding.</li>
<li>In all following commands, replace <code>sdX</code> with your actual device name (e.g., <code>sda</code>, <code>nvme0n1</code>). You can use the <code>lsblk</code> command to list all available block devices.</li>
<li>You are free to partition the disk according to your needs. However, the layout below is a rather optimal setup for an older computer with limited disk space.</li>
<li>The size of the swap partition should correspond to the amount of RAM in your system. In this guide, we will create a swap partition equal to system’s memory size.</li>
</ol>
<p>To remove existing partitions use the following commands:</p>
<pre><code class="hljs language-shell">sudo parted /dev/sdX -- print # prints existing partitions
sudo parted /dev/sdX -- rm n # remove partition with number n
</code></pre>
<p>Create new partitions:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">Create partition table GPT</span>
sudo parted /dev/sdX -- mklabel gpt 
<span class="hljs-meta prompt_">
# </span><span class="language-bash">Create boot partition</span>
sudo parted /dev/sdX -- mkpart ESP fat32 1Mib 512Mib
sudo parted /dev/sdX -- set 1 esp on
<span class="hljs-meta prompt_">
# </span><span class="language-bash">Create swap partition (size 2048 MiB)</span>
sudo parted /dev/sdX -- mkpart primary linux-swap 512MiB 2560MiB
<span class="hljs-meta prompt_">
# </span><span class="language-bash">Create root partition</span>
sudo parted /dev/sdX -- mkpart primary ext4 2560MiB 100%
</code></pre>
<p>Format new partitions:</p>
<pre><code class="hljs language-shell">sudo mkfs.fat -F 32 -n boot /dev/sdX1
sudo mkswap -L swap /dev/sdX2
sudo mkfs.ext4 -L nixos /dev/sdX3
</code></pre>
<p>Mount partitions:</p>
<pre><code class="hljs language-shell">sudo mount /dev/disk/by-label/nixos /mnt

sudo mkdir -p /mnt/boot
sudo mount /dev/disk/by-label/boot /mnt/boot

sudo swapon /dev/disk/by-label/swap
</code></pre>
<h4 id="create-nixos-configuration" tabindex="-1">Create NixOS configuration</h4>
<pre><code class="hljs language-shell">sudo nixos-generate-config --root /mnt
</code></pre>
<p>After that open <code>/mnt/etc/nixos/hardware-configuration.nix</code></p>
<pre><code class="hljs language-shell">sudo -e /mnt/etc/nixos/hardware-configuration.nix
</code></pre>
<p>If you use <code>b43</code> Wi-Fi driver then make sure that <code>broadcom_sta</code> is not set up.</p>
<p>Add <code>noatime</code> option to root file system configuration.</p>
<p>Replace the entire contents of <code>/mnt/etc/nixos/configuration.nix</code> with the following configuration. Adjust it according to your needs.</p>
<pre><code class="hljs language-nix"><span class="hljs-comment"># This configuration.nix file is optimized for an old (2007) iMac.</span>
<span class="hljs-comment"># It can be adapted for any old computer with weak hardware.</span>
<span class="hljs-comment"># To do this, you just need to configure it for your specific hardware:</span>
<span class="hljs-comment"># Wi-Fi card, video driver, printer, etc. Along with that, you may</span>
<span class="hljs-comment"># choose your preferences for locale, basic system software, themes, etc.</span>
{ config, lib, pkgs, ... }:

{
  <span class="hljs-attr">imports</span> <span class="hljs-operator">=</span> [ <span class="hljs-symbol">./hardware-configuration.nix</span> ];

  <span class="hljs-attr">hardware</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># Wi-Fi card and Bluetooth on iMac require redistributable firmware</span>
    <span class="hljs-attr">enableRedistributableFirmware</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-comment"># Enable harware graphics support</span>
    <span class="hljs-attr">graphics</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">enable32Bit</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    };
    <span class="hljs-comment"># Enable Bluetooth support</span>
    <span class="hljs-attr">bluetooth</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">powerOnBoot</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">package</span> <span class="hljs-operator">=</span> pkgs.bluez;
      <span class="hljs-attr">settings</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">General</span> <span class="hljs-operator">=</span> {
          <span class="hljs-attr">Experimental</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
	        <span class="hljs-attr">ControllerMode</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;dual&quot;</span>;
	        <span class="hljs-attr">FastConnectable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
	      };
      };
    };
  };

  <span class="hljs-attr">boot</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># Use EFI bootloader</span>
    <span class="hljs-attr">loader</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">systemd-boot</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        <span class="hljs-attr">configurationLimit</span> <span class="hljs-operator">=</span> <span class="hljs-number">5</span>;
      };
      <span class="hljs-attr">efi.canTouchEfiVariables</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    };
  };
  <span class="hljs-comment"># Enable compressed RAM swap. Useful for low RAM systems</span>
  <span class="hljs-attr">zramSwap</span> <span class="hljs-operator">=</span> {
    <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-comment"># Allow utilizing 60% of RAM for compressed swap</span>
    <span class="hljs-attr">memoryPercent</span> <span class="hljs-operator">=</span> <span class="hljs-number">60</span>;
  };
  <span class="hljs-comment"># Configure networking</span>
  <span class="hljs-attr">networking</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># Use network manager</span>
    <span class="hljs-attr">networkmanager.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-comment"># Set desired host name</span>
    <span class="hljs-attr">hostName</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;nixos&quot;</span>;
    <span class="hljs-comment"># Enable firmware to make Wi-Fi card working (in my case BCM4321)</span>
    <span class="hljs-attr">enableB43Firmware</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
  };
  <span class="hljs-comment"># Setup time zone and locale</span>
  <span class="hljs-attr">time.timeZone</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;Europe/Moscow&quot;</span>;
  <span class="hljs-attr">i18n.defaultLocale</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;ru_RU.UTF-8&quot;</span>;
  <span class="hljs-comment"># Configure services</span>
  <span class="hljs-attr">services</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># GUI</span>
    <span class="hljs-attr">xserver</span> <span class="hljs-operator">=</span> {
      <span class="hljs-comment"># Enable X server</span>
      <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-comment"># Setup video driver. Old iMac has Radeon graphics card</span>
      <span class="hljs-attr">videoDrivers</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;radeon&quot;</span> ];
      <span class="hljs-comment"># Configure lightweight display manager</span>
      <span class="hljs-attr">displayManager</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">lightdm</span> <span class="hljs-operator">=</span> {
	        <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
          <span class="hljs-attr">greeters.gtk.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
	      };
        <span class="hljs-comment"># The following commands are to configure a good-looking default GUI</span>
        <span class="hljs-comment"># theme. The script does not change the user settings made manually.</span>
        <span class="hljs-attr">sessionCommands</span> <span class="hljs-operator">=</span> <span class="hljs-string">&#x27;&#x27;
          set_default() {
            local ch=&quot;$1&quot; key=&quot;$2&quot; val=&quot;$3&quot; type=&quot;$4&quot;
            if ! xfconf-query -c &quot;$ch&quot; -p &quot;$key&quot; &gt;/dev/null 2&gt;&amp;1; then
              xfconf-query -c &quot;$ch&quot; -p &quot;$key&quot; -s &quot;$val&quot; --create -t &quot;$type&quot;
            fi
          }

          set_default xsettings /Net/ThemeName &quot;Arc-Dark&quot; string
          set_default xsettings /Net/IconThemeName &quot;Papirus-Dark&quot; string
          set_default xsettings /Gtk/CursorThemeName &quot;Bibata-Modern-Classic&quot; string
          set_default xsettings /Gtk/CursorThemeSize &quot;24&quot; int
          set_default xfwm4 /general/theme &quot;Arc-Dark&quot; string
          set_default xsettings /Gtk/FontName &quot;Inter 10&quot; string
        &#x27;&#x27;</span>;
      };
      <span class="hljs-comment"># Setting XFCE as a desktop manager. It is rather lightweight but</span>
      <span class="hljs-comment"># feature-reach</span>
      <span class="hljs-attr">desktopManager.xfce.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-comment"># Configure multi language support. This section could be omitted because</span>
      <span class="hljs-comment"># XFCE supports configuring keyboard layout switching.</span>
      <span class="hljs-attr">xkb</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">layout</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;us,ru&quot;</span>;
        <span class="hljs-comment"># Swithch keyboard layouts with CMD+Space like by default on Mac</span>
        <span class="hljs-attr">options</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;grp:win_space_toggle&quot;</span>;
      };
    };
    <span class="hljs-comment"># Configure autologin for your user. Weak computers usually do not intended</span>
    <span class="hljs-comment"># to be used by several users. So this configuration can be useful. If you</span>
    <span class="hljs-comment"># want more than one user just omit this section.</span>
    <span class="hljs-attr">displayManager</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">autoLogin</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        <span class="hljs-attr">user</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;your-user-name&quot;</span>;
      };
    };
    <span class="hljs-comment"># Configure multimedia support</span>
    <span class="hljs-attr">pipewire</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-comment"># Enable alsa comatibility</span>
      <span class="hljs-attr">alsa.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-comment"># Enable pulse audio comatibility</span>
      <span class="hljs-attr">pulse.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

      <span class="hljs-attr">wireplumber</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        <span class="hljs-comment"># Configure audio support via Bluetooth</span>
	      <span class="hljs-attr">extraConfig</span> <span class="hljs-operator">=</span> {
          <span class="hljs-string">&quot;50-bluez&quot;</span> <span class="hljs-operator">=</span> {
            <span class="hljs-attr">monitor.bluez.properties</span> <span class="hljs-operator">=</span> {
              <span class="hljs-attr">bluez5</span> <span class="hljs-operator">=</span> {
	              <span class="hljs-attr">enable-msbc</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
                <span class="hljs-attr">enable-sbc-xq</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
		            <span class="hljs-attr">enable-hw-volume</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
		            <span class="hljs-attr">roles</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;a2dp_sink&quot;</span> <span class="hljs-string">&quot;a2dp_source&quot;</span> <span class="hljs-string">&quot;hsp_hs&quot;</span> <span class="hljs-string">&quot;hfp_hf&quot;</span> ];
	            };
	          };
	        };
	      };
      };
    };
    <span class="hljs-comment"># Enable power management</span>
    <span class="hljs-attr">tlp.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-comment"># Enable daemon for temperature monitoring</span>
    <span class="hljs-attr">thermald.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-comment"># Enable SSH.</span>
    <span class="hljs-attr">openssh.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-comment"># Configure printing</span>
    <span class="hljs-attr">printing</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-comment"># Setup correct drivers. The following driver is for most Epson printers.</span>
      <span class="hljs-attr">drivers</span> <span class="hljs-operator">=</span> [ pkgs.epson-escpr ];
    };

    <span class="hljs-attr">avahi</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">nssmdns4</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">openFirewall</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    };
    <span class="hljs-comment"># Enable Bluetooth manager</span>
    <span class="hljs-attr">blueman.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
  };
  <span class="hljs-comment"># Configure fonts</span>
  <span class="hljs-attr">fonts</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># Install most useful and popular fonts</span>
    <span class="hljs-attr">packages</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">with</span> pkgs; [
      dejavu_fonts
      liberation_ttf
      noto-fonts noto-fonts-cjk-sans noto-fonts-emoji
      inter
      roboto roboto-mono
      jetbrains-mono
      fira-code
      font-awesome
      corefonts
    ];
    <span class="hljs-comment"># Font settings</span>
    <span class="hljs-attr">fontconfig</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">antialias</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">hinting.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">hinting.style</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;slight&quot;</span>;
      <span class="hljs-attr">subpixel.rgba</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;rgb&quot;</span>;
      <span class="hljs-attr">subpixel.lcdfilter</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;default&quot;</span>;

      <span class="hljs-attr">defaultFonts</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">serif</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;Noto Serif&quot;</span> <span class="hljs-string">&quot;DejaVu Serif&quot;</span> <span class="hljs-string">&quot;Liberation Serif&quot;</span> ];
        <span class="hljs-attr">sansSerif</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;Inter&quot;</span> <span class="hljs-string">&quot;Noto Sans&quot;</span> <span class="hljs-string">&quot;DejaVu Sans&quot;</span> <span class="hljs-string">&quot;Liberation Sans&quot;</span> ];
        <span class="hljs-attr">monospace</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;JetBains Mono&quot;</span> <span class="hljs-string">&quot;Fira Code&quot;</span> <span class="hljs-string">&quot;DejaVu Sans Mono&quot;</span> <span class="hljs-string">&quot;Roboto Mono&quot;</span> ];
        <span class="hljs-attr">emoji</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;Noto Color Emoji&quot;</span> ];
      };
    };
  };
  <span class="hljs-comment"># By default XFCE installs thunar (file manager), but does not install</span>
  <span class="hljs-comment"># thunar-archive-plugin. Here we explicitly install thunar with plugins.</span>
  <span class="hljs-comment"># Having thunar-archive-plugin allows us cpmpress/decompress files from the</span>
  <span class="hljs-comment"># context menu.</span>
  <span class="hljs-attr">programs.thunar</span> <span class="hljs-operator">=</span> {
    <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-attr">plugins</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">with</span> pkgs.xfce; [
      thunar-archive-plugin
      thunar-volman
    ];
  };
  <span class="hljs-comment"># At the moment of creating this configuration file there is an issue with</span>
  <span class="hljs-comment"># thunar-archive-plugin: it does not work with xarchiver. The following hack</span>
  <span class="hljs-comment"># resolves the issue.</span>
  <span class="hljs-attr">nixpkgs.overlays</span> <span class="hljs-operator">=</span> [
    (<span class="hljs-params">final:</span> <span class="hljs-params">prev:</span> {
      <span class="hljs-attr">xfce</span><span class="hljs-operator">=</span> prev.xfce.overrideScope (<span class="hljs-params">_self:</span> <span class="hljs-params">super:</span> {
        <span class="hljs-attr">thunar-archive-plugin</span> <span class="hljs-operator">=</span> super.thunar-archive-plugin.overrideAttrs (<span class="hljs-params">old:</span> {
	        <span class="hljs-attr">postInstall</span> <span class="hljs-operator">=</span> lib.concatStringsSep <span class="hljs-string">&quot;<span class="hljs-char escape_">\n</span>&quot;</span> [
	          (old.postInstall <span class="hljs-keyword">or</span> <span class="hljs-string">&quot;&quot;</span>)
            <span class="hljs-string">&#x27;&#x27;
              mkdir -p $out/libexec/thunar-archive-plugin
              cp -r <span class="hljs-subst">${prev.xarchiver}</span>/libexec/thunar-archive-plugin/* \
                    $out/libexec/thunar-archive-plugin/ 2&gt;/dev/null || true
            &#x27;&#x27;</span>
          ];
	      });
      });
    })
  ];
  <span class="hljs-comment"># Install system-wide packages</span>
  <span class="hljs-attr">environment</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># Do not install any package by default. We want full control of packages</span>
    <span class="hljs-comment"># configuration</span>
    <span class="hljs-attr">defaultPackages</span> <span class="hljs-operator">=</span> [];
    <span class="hljs-comment"># We install only the following minimum of packages. Other packages user can</span>
    <span class="hljs-comment"># install in their profile.</span>
    <span class="hljs-attr">systemPackages</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">with</span> pkgs; [
      htop ncdu iotop lm_sensors pciutils usbutils <span class="hljs-comment"># System utilities</span>
      xfce.xfce4-terminal <span class="hljs-comment"># Graphical terminal</span>
      xfce.xfce4-xkb-plugin <span class="hljs-comment"># plugin to display keyboard layout variant in status bar</span>
      rofi <span class="hljs-comment"># An utility for applications quick start and other useful features</span>
      system-config-printer <span class="hljs-comment"># GUI utility for printer settings</span>
      arc-theme papirus-icon-theme bibata-cursors <span class="hljs-comment"># Themes for XFCE</span>
      zathura <span class="hljs-comment"># Document viewer</span>
      mpv <span class="hljs-comment"># Media player</span>
      feh <span class="hljs-comment"># Image viewer</span>
      micro <span class="hljs-comment"># Tiny simple text editor used as default editor</span>
      neovim <span class="hljs-comment"># Text editor for advanced users</span>
      xarchiver zip unzip p7zip xz zstd bzip2 gzip unrar <span class="hljs-comment"># Archiver utilities</span>
      firefox <span class="hljs-comment"># Web browser</span>
    ];
    <span class="hljs-comment"># Set some environment variables</span>
    <span class="hljs-attr">variables</span> <span class="hljs-operator">=</span> {
      <span class="hljs-comment"># Default editor</span>
      <span class="hljs-attr">EDITOR</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;micro&quot;</span>;
      <span class="hljs-comment"># Hope QT applications will look better in XFCE</span>
      <span class="hljs-attr">QT_QPA_PLATFORMTHEME</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;gtk3&quot;</span>;
    };
  };
  <span class="hljs-comment"># Nix configuration</span>
  <span class="hljs-attr">nix</span> <span class="hljs-operator">=</span> {
    <span class="hljs-comment"># Configure garbage collection</span>
    <span class="hljs-attr">gc</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">automatic</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">dates</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;weekly&quot;</span>;
      <span class="hljs-attr">options</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;--delete-older-than 7d&quot;</span>;
    };
    <span class="hljs-comment"># Additional useful settings</span>
    <span class="hljs-attr">settings</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">auto-optimise-store</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">experimental-features</span> <span class="hljs-operator">=</span> [ <span class="hljs-string">&quot;nix-command&quot;</span> <span class="hljs-string">&quot;flakes&quot;</span> ];
    };
  };
  <span class="hljs-comment"># Allow proprietary software</span>
  <span class="hljs-attr">nixpkgs.config.allowUnfree</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
  <span class="hljs-comment"># Users configuration</span>
  <span class="hljs-attr">users.users.your-user-name</span> <span class="hljs-operator">=</span> {
    <span class="hljs-attr">isNormalUser</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-attr">extraGroups</span> <span class="hljs-operator">=</span> [
      <span class="hljs-string">&quot;wheel&quot;</span> <span class="hljs-comment"># Enables sudo for user</span>
      <span class="hljs-string">&quot;networkmanager&quot;</span> <span class="hljs-comment"># Enable Wi-Fi connections for user</span>
    ];
  };
  <span class="hljs-comment"># Enable sudo</span>
  <span class="hljs-attr">security.sudo.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

  <span class="hljs-attr">system.stateVersion</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;25.05&quot;</span>;
}
</code></pre>
<h4 id="install-the-system" tabindex="-1">Install the system</h4>
<pre><code class="hljs language-shell">sudo nixos-install
</code></pre>
<p>After the installation process finishes, reboot the system and enjoy. Of course, you can fine-tune the system now as you like.</p>
]]></content:encoded>
            <author>hi+alexeydanilevsky@serokell.co (Alexey Danilevsky)</author>
            <enclosure url="https://serokell.co/files/al/thumb.alt8718.normal-Reviving_an_Old_iMac_with_NixOS.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Haskell in Production: Scrive]]></title>
            <link>https://serokell.co/blog/haskell-in-production-scrive</link>
            <guid isPermaLink="false">https://serokell.co/blog/haskell-in-production-scrive</guid>
            <pubDate>Tue, 29 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[ In our Haskell in Production series, we interview developers and technical leaders from companies that use Haskell for real-world tasks. We cover benefits, downsides, common pitfalls, and tips for b…]]></description>
            <content:encoded><![CDATA[<p>In our Haskell in Production series, we interview developers and technical leaders from companies that use Haskell for real-world tasks. We cover benefits, downsides, common pitfalls, and tips for building useful Haskell products. Today’s guest is Flavio Corpa, the Senior Software Engineer of Scrive – a company providing electronic signature services and eID solutions.</p>
<p><img src="https://serokell.co/files/ad/ad3upzc.personal_card-Fla%CC%81vio_Corpa.jpg" alt="ad3upzc.personal_card-Flávio_Corpa.jpg"></p>
<h1 id="technical-stack-%26-architecture" tabindex="-1">Technical Stack &amp; Architecture</h1>
<p><strong>What was the architecture of Scrive like when Haskell was first introduced? What kind of problems was it intended to solve at that point?</strong></p>
<p>One of the founders, Gracjan Polak, was a functional programming enthusiast. He started programming Scrive using Haskell for the backend from the very beginning.</p>
<p><strong>Which parts of your system are written in Haskell today, and where did you deliberately choose not to use it?</strong></p>
<p>Most of the electronic signature and eID backend is written in Haskell, with a notable exception in the final PDF handling logic, where the iText library is used via Kotlin.</p>
<p><strong>Were there any unexpected advantages or friction points when using Haskell in a highly regulated domain, with legal traceability and audit requirements?</strong></p>
<p>In audits, we have to respond to questions like: “Which static code analysis are you using?”. It is a bit hard to explain why Haskell does not have any.</p>
<p><strong>Do you use AI for Haskell development? Could you share your experience with it? Is it helpful? How do you think the rapid growth of programming AI will affect the future of Haskell?</strong></p>
<p>Yes the company encourages us to use AI in our Haskell development and provides us with Github Copilot licenses, I have been using it recently but also besides my job I’ve been using tools like Cursor and Windsurf for developing with TypeScript and Svelte and I have to say I feel like the support of LLMs for Haskell is quite limited in my view. This was to be expected because of the reduced sample size of the dataset we have compared to other mainstream languages, but I really hope they will become smarter in the future and aid us even in such complex languages as Haskell!</p>
<p><strong>What build and deployment tools do you use in dev environments and for CI/CD?</strong></p>
<p>We have a mix of Docker and Nix company-wise, but mostly our CI/CD is Docker with Kubernetes, and it is working fine for us, but I would say I feel it is a bit slower than it should be. Definitely, there’s room for improvement there.</p>
<h1 id="scale%2C-interfacing-%26-observability" tabindex="-1">Scale, Interfacing &amp; Observability</h1>
<p><strong>Scrive integrates with many external partners — from banks to government entities. How has Haskell held up when dealing with complex, real-world API interactions? Context: Digging into serialization, FFI, latency, and error resilience.</strong></p>
<p>There was a point where we had to hand-roll a SOAP client in Haskell for an external integration :) But overall, we mostly find what we need in the ecosystem - sometimes resorting to maintaining some of the packages (for example, hpqtypes).</p>
<p><strong>How do you handle observability in your Haskell services — logging, tracing, metrics? Was this an area where you had to build extra tooling or abstractions?</strong></p>
<p>We use Prometheus through the prometheus-haskell suite of packages.
Logging is done with our own open-source toolkit: <a href="https://github.com/scrive/log">https://github.com/scrive/log</a>.
Metrics are produced with a fork of the tracing library and sent to Grafana Tempo.</p>
<h1 id="haskell-in-production%3A-lessons-and-tactics" tabindex="-1">Haskell in Production: Lessons and Tactics</h1>
<p><strong>What are some hard-earned lessons from maintaining Haskell in production at scale? Any anti-patterns you’d warn others to avoid?</strong></p>
<p>Functions that acquire locks on the database can be at the root of production issues if they are not well-understood by developers, and this starts with the name. A function called withLockedDocument will be used more carefully than a function simply called withDocument.*
Trying to fit several entities that have admittedly similar shapes, but wildly different life cycles, into one type will come to haunt you sooner than you think. You can resort to a number of tricks to express two different types of users in the same data type, but this will put a burden on your code. Write a new data type; it’s free.
In some parts of the codebase, we are using effectful, and we are very happy with it, btw ;)</p>
<p><strong>How do you onboard new engineers who aren’t experienced in Haskell? Has this influenced how you write or structure code internally?</strong></p>
<p>Mob and pair programming are a huge thing within Scrive, even engineers who were once QA’s are invited to try out Haskell (and Elm), and they sometimes even stick to it! Since the company uses mostly “Boring Haskell” (except for a few complex abstractions here and there), the onboarding process is not so difficult once the developer starts to understand Haskell’s basic principles and syntax.</p>
<p><strong>Have you built any internal libraries or DSLs to help model domain logic (e.g., document flows, signing rules, legal states)?</strong></p>
<p>Yes! I joined a team within Scrive that had built its own DSL following the recommendation of Sandy Maguire in his book Algebra Driven Design (which I had to read, of course, haha). The domain was very specific to documents, signatures, and the way we handle them in the company, but the resulting DSL and property tests written as a consequence were really nice and incredibly powerful!</p>
<p><strong>How do you approach GHC upgrades? Do you usually try to keep up with the latest stable GHC version, or do you stay on the version that works for you for as long as you can? How painful are GHC upgrades for you? Do they require a lot of work to build your whole codebase?</strong></p>
<p>We upgrade to new minor versions of GHC based on how well-supported they are by the ecosystem and known regressions, the latter being the main adoption blockers for us.</p>
<p><strong>Do you follow GHC development with regard to new features? Are you open to introducing them into your code, or do you prefer being more conservative? Are there any features added over the last few years that you find particularly useful?</strong></p>
<p>We keep an eye out for the latest improvements in features that allow us to debug our systems in production in an efficient manner, like thread labels, backtraces, and callstacks. In this regard, GHC 9.12 brings many good things. We were very happy to adopt OverloadedRecordDot, which decreased our usage of the Optics library a lot.</p>
<h1 id="functional-mindset-vs-business-constraints" tabindex="-1">Functional Mindset vs Business Constraints</h1>
<p><strong>How do you balance functional purity with product velocity, especially in a legal-tech setting where correctness is critical but deadlines are real?</strong></p>
<p>Our tooling and performance team exists to lay reliable foundations upon which we can build rapidly. Percentage-wise, the majority of “new” features are built upon existing features. We rarely have to rebuild something from the ground up.
Moreover, our QA team ensures that what we deliver is up to spec. They maintain their own list of invariants to check for, and they are a big reason why we deliver quality software today.</p>
<p><strong>Do you feel that using Haskell has shaped the way your team thinks about problems, not just technically, but organizationally or culturally?</strong></p>
<p>I think we are using Haskell in a practical way and not an academic way. Outside the scope of a single service, I see no difference from other tech companies that could possibly be influenced by tech choice. Having said that, working in a company full of functional developers (frontend and backend) is quite a delight to be honest :)</p>
<h1 id="the-road-ahead" tabindex="-1">The Road Ahead</h1>
<p><strong>Are there areas of Scrive’s platform where you’re planning to increase (or reduce) Haskell usage in the future?</strong></p>
<p>While backend services for the main product suite keep being written in Haskell, we are also letting other teams (integrations, analytics) use the tools they are comfortable with.</p>
<p><strong>If you had to rebuild a critical part of your system today, would you still choose Haskell, or would another tool be more appropriate now?</strong></p>
<p>Considering our investments in development experience and the in-house expertise: Yes.</p>
<hr>
<p>Thanks Flávio for this interview!
<a href="https://flaviocorpa.com/index.html">Flávio website</a>
<a href="https://x.com/FlavioCorpa">Flávio Twitter</a>
<a href="https://www.scrive.com/">Scrive website</a>
<a href="https://www.linkedin.com/company/scrive/?originalSubdomain=se">Scrive LinkedIn</a></p>
]]></content:encoded>
            <author>hi+denisoleynikov@serokell.co (Denis Oleynikov)</author>
            <enclosure url="https://serokell.co/files/a8/thumb.a8hrsg0.normal-Flávio_Corpa.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[A Bit Late but Ultimate Analysis: DeepSeek]]></title>
            <link>https://serokell.co/blog/a-bit-late-but-ultimate-analysis-deepseek</link>
            <guid isPermaLink="false">https://serokell.co/blog/a-bit-late-but-ultimate-analysis-deepseek</guid>
            <pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[ tldr: if you want to find out everything about deepseek you can read this blogpost that has 3 separate sections with increasing technical details and difficulty from one to another, so you can stop …]]></description>
            <content:encoded><![CDATA[<h1 id="deepseek-r1" tabindex="-1">DeepSeek R1</h1>
<p><em>tldr: if you want to find out everything about deepseek you can read this blogpost that has 3 separate sections with increasing technical details and difficulty from one to another, so you can stop your reading at any point once you’ve got enough information or material becomes too difficult to comprehend.</em></p>
<p>Lately DeepSeek released <a href="https://api-docs.deepseek.com/news/news250120">their latest model R1</a> which has performance comparable with all the latest available OpenAI models while having much less computational costs. Unfortunately due to a lot of optimistic claims by their team and a lot of difficult to comprehend innovations introduced in their work, we’ve got a lot of rumours and misunderstanding circling around this mode.</p>
<p>In our blogpost we will briefly break down most common rumours and speculations about R1 model, give detailed but easily comprehensible explanations of all DeepSeek innovations in this model and explain why it was so cheap to train and so easy to operate, and in the end provide some deeper explanation on the most difficult parts of their research, so you could understand how it works up until the last bit.</p>
<h1 id="rumours-and-speculations-breakdown" tabindex="-1">Rumours and speculations breakdown</h1>
<ol>
<li><em>“DeepSeek R1 is on the same level as OpenAI models, but much cheaper!”</em> While DeepSeek’s inference is definitely much cheaper, it’s performance excellence is not so clear. Yes, it shows comparable or better performance than some OpenAI’s models on several open benchmarks, but this holds true only for math and coding, it shows much worse results for other common tasks. Also there are some independent researches that it is worse for more general math and coding tasks outside of popular benchmarks, which was partially confirmed on latest AIME competition (see Data Labelling Pipeline NB for details). I definitely recommend to think about this model more as Google Gemini Flash Thinking competitor, than full-fledged OpenAI model’s.</li>
<li><em>“DeepSeek is dirt-cheap to use!”</em> Well, yes and no. Yes, you can use DeepSeek model from their official API for the fraction of the cost of other popular models like LLama. But unfortunately their team was not ready for such a hype, so their API is down very often and very unstable to use. And if you will try to use it internally or buy some other APIs that run it, you will quickly find out that it is several times more expensive to do. The main problem is that while weights of the model and white paper about it were openly published, their hardware-specific source code was not. And it contains tons of optimizations that make this model cheaper to run.</li>
<li><em>“DeepSeek stole OpenAI’s data!”</em> From what we are seeing from our internal and other independent tests this statement seems quite unlikely to be true and probably were made to cool down OpenAI’s investors. Later in the second section you will see some details on their innovative technique to gather data, provided in the DeepSeekMath paper. And in third section we will discuss how this technique was further improved and changed to make a DeepSeek-Zero and then DeepSeek-R1 model. These innovations are also contradict that initial OpenAI’s statement.</li>
<li><em>“DeepSeek spent <strong>5.58 million</strong> to train — over 89 times cheaper than OpenAI’s rumored 500 million budget for its o1 model!”</em> Well, that’s complete nonsense. While 5.58 mil is probably a true number and it is much cheaper than competitors, we are talking about 4-8 times difference at most. The main issue is that 5.58 mil was spent only for a single final training run of the model, which for example for other comparable sized models with known costs were in between 7 to 20 mil. This price tag does not incorporate all intermediate runs, which are usually much cheaper, but there are up to several hundreds of them. It also does not include data gathering, research, development and human resources spendings. So in the end completely developed DeepSeek model probably costed at least 200 millions. Nevertheless, they provided a lot of innovations to reduce both the training and inference costs, which we discuss later in this blogpost.</li>
</ol>
<h1 id="innovations-breakdown" tabindex="-1">Innovations breakdown</h1>
<p>Now let’s take a look at all optimisations and innovations made by DeepSeek. I will mostly focus on either general scientific achievements or technical cost-reduction innovations. This section is still general-public oriented, so I hope it will be easy to digest.</p>
<h3 id="1.-low-level-optimization-for-faster-computation" tabindex="-1"><strong>1. Low-Level Optimization for Faster Computation</strong></h3>
<p>Most AI models are trained using <strong>PyTorch</strong>, a popular deep-learning framework that provides ease of use but adds extra computational overhead. For faster training, many advanced AI teams use <strong>NVIDIA’s NCCL</strong> instead (a high-performance library for communication between GPUs). However, DeepSeek went even deeper — they <strong>customized NCCL itself</strong>, optimizing GPU <strong>Streaming Multiprocessors (SMs)</strong> using super low level <strong>PTX (Parallel Thread Execution) assembly language</strong>. This super low-level tuning allowed them to better match their specific hardware architecture, reducing latency and improving data transfer between GPUs. This approach was introduced in their <strong>DeepSeek V2</strong> paper.</p>
<h3 id="2.-8-bit-hybrid-training-instead-of-32-bit-for-cost-efficiency" tabindex="-1"><strong>2. 8-bit hybrid Training Instead of 32-bit for Cost Efficiency</strong></h3>
<p>Most AI models train in <strong>32-bit floating point (FP32) or 16-bit floating point (FP16)</strong> precision. This is a standard approach that ensures stability but requires significant computational power. DeepSeek was able to <strong>stabilize 8-bit training (FP8)</strong>, drastically cutting memory usage and increasing speed. But they didn’t just naively apply 8-bit across the board which is well known to be unstable. They used a <strong>hybrid approach</strong> where most layers operated in <strong>FP8</strong>, but some carefully picked ones were aggregated in <strong>32-bit precision</strong> when needed for stability. This <strong>“Floating Point Adaptive” (FPA) training</strong> balances efficiency and accuracy while reducing training costs and memory requirements.</p>
<h3 id="3.-mixture-of-experts-(moe)-for-massive-parameter-efficiency" tabindex="-1"><strong>3. Mixture of Experts (MoE) for Massive Parameter Efficiency</strong></h3>
<p>DeepSeek R1 uses a <strong>Mixture of Experts (MoE) architecture</strong>, meaning that instead of activating all <strong>671 billion</strong> parameters during inference, it selectively activates only <strong>37 billion</strong>. This drastically reduces computational load while still leveraging a large model’s capability. While MoE approach itself is well-known and already were used by OpenAI and Mistral models, they gave an extra spin on it. MoE introduces a new challenge — <strong>balancing the GPU workload</strong>. Since only a subset of experts is active at any given time, <strong>not all GPUs are used equally</strong>, and some of them are basically idling and waiting for data. Instead of relying on <strong>NVIDIA’s default load management</strong>, DeepSeek developed a <strong>custom load balancer</strong> to optimally distribute work across concrete GPUs infrastructure they had according to their specific architecture.</p>
<h3 id="4.-optimized-hardware-choices-for-us-export-limited-gpus" tabindex="-1"><strong>4. Optimized Hardware Choices for US Export-Limited GPUs</strong></h3>
<p>Training and running large models depend on three key factors:</p>
<ul>
<li><strong>Compute power (FLOPs)</strong> – Main speed multiplier for training base LLMs.</li>
<li><strong>Memory bandwidth</strong> – How fast GPUs can access and process data.</li>
<li><strong>Interconnect speed</strong> – How efficiently GPUs communicate with each other.</li>
</ul>
<p>Due to <strong>US export restrictions</strong>, DeepSeek was unable to access the highest-end NVIDIA GPUs, which limited them in <strong>FLOPs</strong>. However, they made up for this by <strong>NVIDIA providing specialized cards with high memory bandwidth and fast interconnect speeds</strong>, much higher than their top performing server GPUs. This turned out to be <strong>more important for reasoning models</strong> (models optimized for tasks like problem-solving and step-by-step reasoning rather than raw number crunching), which DeepSeek-R1 is. So unintentionally NVIDIA helped them to overcome US Export limitations, at least for their reasoning model. I assume that this might result into additional restrictions later.</p>
<h3 id="5.-efficient-attention-mechanism%3A-mla-attention" tabindex="-1"><strong>5. Efficient Attention Mechanism: MLA Attention</strong></h3>
<p>Traditional Transformer models, like those introduced in the famous <em>“Attention is All You Need”</em> paper, use <strong>quadratic complexity</strong> for attention mechanisms, meaning computational cost grows rapidly with longer input sequences. DeepSeek R1 uses <strong>Multi-Layer Aggregation (MLA) Attention</strong>, which allows it to <strong>reduce complexity</strong> by leveraging <strong>fewer latent representations</strong> while maintaining accuracy. This helps improve speed and scalability when processing large inputs. Moreover they once again did it with a low-level hardware-specific implementation, this approach showed up to 50% performance boost in attention calculations when was applied by other AI labs, so it is probably comparable here.</p>
<h3 id="6.-from-trpo-to-ppo-and-grpo%3A-evolution-of-reinforcement-learning" tabindex="-1"><strong>6. From TRPO to PPO and GRPO: Evolution of Reinforcement Learning</strong></h3>
<p>DeepSeek R1 improves training stability by leveraging <strong>policy optimization techniques in reinforcement learning</strong>. Originally, <strong>Trust Region Policy Optimization (TRPO)</strong> was used in many RL-based training approaches, but it had limitations — it imposed strict constraints that could slow down learning. The transition to <strong>Proximal Policy Optimization (PPO)</strong> relaxed these constraints while maintaining stability, making it more efficient for fine-tuning AI models. The main issue with PPO was in it’s should store additional model that is needed to approximate special value function that is used to optimise LLMs parameters. DeepSeek introduced novel approach called <strong>Group Relative Policy Optimization (GRPO)</strong> based on PPO which completely excludes this costly requirement. For more details on this approach you can look at the last section of this blogpost.</p>
<h3 id="7.-self-learning-with-automated-rule-based-rewards" tabindex="-1"><strong>7. Self-Learning with Automated Rule-Based Rewards</strong></h3>
<p>While it is not really related to the cost of the final training run, or inference costs, one of DeepSeek’s most <strong>cost-effective strategies</strong> was minimizing human intervention in fine-tuning. Instead of relying heavily on <strong>Reinforcement Learning from Human Feedback (RLHF)</strong> (which requires expensive human labelers), they introduced a <strong>rule-based self-learning system</strong> with two types of rewards:</p>
<ul>
<li><strong>Accuracy Rewards</strong> – For tasks with clear right/wrong answers (e.g., math problems, programming challenges), the system <strong>automatically evaluates correctness</strong> using predefined test cases or expected formats.</li>
<li><strong>Format Rewards</strong> – The model was trained to structure its reasoning process clearly by placing intermediate thoughts between <code>&lt;think&gt;</code> and <code>&lt;/think&gt;</code> tags, making its responses more interpretable.</li>
</ul>
<p>This <strong>automation reduced costs</strong> while surprisingly maintaining high-quality learning outcomes. While the idea of this approach is not novel, model was able to effectively train  itself to reason from the ground up, <strong>which was not properly achieved before</strong>. I will focus more on the whole pipeline in the next section.</p>
<h3 id="8.-complicated-but-efficient-dataset-generation-and-r1-training-pipelines" tabindex="-1">8. Complicated but efficient dataset generation and R1 training pipelines</h3>
<p>In their work they used original DeepSeekMath paper as a starting point. In that paper they utilised open Common Crawl repository and expanded it with multiple iterations through the semi-automated approach using old-fashioned FastText model for webpages filtering and annotating them. As a result they obtained good reasoning dataset which had math and programming problems. These kind of problems not only has some internal reasoning, but this reasoning is possible to validate automatically.</p>
<p>DeepSeekMath showed outstanding performance in math and programming tasks within its weight class. From there they trained DeepSeek-R1-Zero model using  prompt and applying automated rewards you’ve seen in previous point. Unfortunately DeepSeek-R1-Zero was mixing languages in its thinking process, so they have to perform extra steps in order to obtain DeepSeek-R1. You can get more technical details in the next section.
<p>This approach excluded both Supervised Fine Tuning (SFT) — a process of using big specially labelled dataset (in this case with handcrafted reasoning chains) to train the initial model. Also it excluded Reinforcement Learning from Human Feedback (RLHF) from the process — it is a long process of running model again and again and using humans to evaluate its outputs. As you can imagine both of these processes are quite costly.</p>
<h3 id="9.-potentially-lower-safety-standards%3F" tabindex="-1"><strong>9. Potentially Lower Safety Standards?</strong></h3>
<p>Some experts speculate that DeepSeek R1 was able to ship faster and more affordably by <strong>cutting back on certain safety features</strong>. One indicator is that the model sometimes incorrectly identifies itself as “ChatGPT” instead of “DeepSeek,” suggesting that <strong>less effort was spent on refining safety guardrails and brand-specific fine-tuning</strong>. This makes sense for an <strong>open-source model</strong>, where users are expected to modify and adapt the AI themselves. Also this model definitely has almost no safeguards and produces harmful and discriminatory outputs with ease, so much less resources were spent there. But maybe it is even better for some applications, try to automatically translate dubs for any TV show where main characters are swearing a lot with OpenAI, you will get rejected pretty fast. Just to be clear: DeepSeek’s official API still has some extra guardrails incorporated, but most of them are not in the model weights themselves.</p>
<h1 id="technical-breakdown" tabindex="-1">Technical breakdown</h1>
<p>In this section we will focus on some deeper technical details that will give you better perspective on some innovations and math behind the scenes and also provide some extra evidence on their corpus and research both being novel, contradicting some of OpenAI’s claims.</p>
<h3 id="data-labelling-pipeline-and-deepseek-r1-zero" tabindex="-1">Data Labelling Pipeline and DeepSeek-R1-Zero</h3>
<p>As a foundation for their data labelling DeepSeek-R1 used DeepSekMath corpus which was constructed from the Common Crawl open dataset. In their paper they provide this picture of iterative pipeline.</p>
<p><img src="https://serokell.co/files/ad/ady1ely.image_(29).png" alt="ady1ely.image_(29).png"></p>
<ul>
<li>It starts with an initial seed corpus OpeWebMath dataset. It is a small high-quality math dataset.</li>
<li>Then, they trained simple and lightweight fastText model (from 2016!) using 500k data points from it as a positive examples database, and using the same number of web pages form Common Crawl as negative ones.</li>
<li>In the next step they applied this model to find deduplicated URLs (i.e. pages with the same URL prefix were merged into one point) that lead to math-related pages preserving only top-ranking ones.</li>
<li>As initial dataset lacked diversity, their next step was to find “disjoint domains”, i.e. internet resources where some percentage of web-pages were math-related.</li>
<li>After finding these domains they were labeled manually, adding more positive examples to the positive corpus and the cycle starts over again with the new math seed.</li>
</ul>
<p><strong>NB.</strong> Some of these websites contains tasks from known benchmarks. DeepSeek’s team applied extra filtering to avoid benchmark contamination in their training data, but as latest American Invitational Mathematics Examination (AIME) competition showed, although all models saw a notable decline in performance, R1 suffered a far greater drop. This might be a signal that they still had a benchmark contamination of some degree.</p>
<h3 id="obtaining-deepseek-r1" tabindex="-1">Obtaining DeepSeek-R1</h3>
<p>First model they have created was DeepSeek-R1-Zero. Basically they took DeepSeek-V3, took their math and code dataset and trained it with this prompt using simple Rule-Based RL training:</p>
<p><img src="https://serokell.co/files/at/atfnm8i.image_(30).png" alt="atfnm8i.image_(30).png"></p>
<p>They used the same reward model I’ve showed in point 7 at previous section.</p>
<p>From that point they have to transition to R1. Why do we need to have a such complicated pipeline instead of just simply using DeepSeek-R1-Zero once we’ve got it? Unfortunately this model suffers both from poor readability and English and Chinese languages mixing. While test showed that single-language restriction reduced benchmarks metrics, it still was a preferable way to go, as the main point of this model is to show proper and understandable reasoning process behind the answer.</p>
<p>Before moving forward just a small reminder: Reinforcement Learning (RL) is a machine learning approach where an agent learns to make <strong>decisions</strong> by performing <strong>actions</strong> and receiving <strong>feedback</strong> in the form of <strong>rewards</strong> or <strong>penalties</strong>, aiming to <strong>maximize cumulative rewards</strong> over time.</p>
<ol>
<li>It starts with a pre-trained DeepSeek-V3 which is an LLM trained in a standard way as all other LLMs, but using optimizations we’ve discussed in previous section.</li>
<li>Perform Supervised Fine Tuning on this V3 model on a carefully selected small set (several thousands samples) of R1-Zero outputs manually validated as high-quality and readable.</li>
<li>Apply the same reasoning self-learning procedure as it was for the R1-Zero using math and coding dataset where auto-validation is possible for the Reinforcement Learning rewards calculation.</li>
<li>Apply rejection sampling. With all generated samples we’ve obtained on the 3-rd step, DeepSeek-V3 used as an external expert that decides which samples should be left. This helps to generate more reasoning chains across more general-purpose domains.</li>
<li>Once again reinforcement learning based training. At this stage some rule-based rewards are applied for areas where it is possible (like math), for others LLM validation is used.</li>
</ol>
<h3 id="deep-dive-into-the-trpo-%E2%86%92-ppo-%E2%86%92-gppo-transition" tabindex="-1">Deep dive into the TRPO → PPO → GPPO transition</h3>
<p>While TRPO and PPO were known in the RL domain, GPPO is completely new and proposed in the DeepSeek-R1 paper. Let’s move from the beginning to understand how it works.</p>
<p>In Reinforcement Learning you usually have some Actor A and some Environment E, E gives you an observation (in this case question q) and A give output (in this case direct answer or a chain of though answer depending on the model). Last element of the schema is the reward that E gives to A depending on the answer quality.</p>
<p>In RL this actor internally will have a neural network (LLM) in our case, in mathematical terms we can call it policy <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><mi>Θ</mi></msub><mo>(</mo><mi>o</mi><mi>b</mi><mi>s</mi><mo>)</mo></mrow><annotation>\pi_{\Theta}(obs)</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">Θ</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">b</span><span class="mord mathnormal">s</span><span class="mclose">)</span></span></span></span>, where <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>Θ</mi></mrow><annotation>\Theta</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord">Θ</span></span></span></span> represents tunable parameters of the LLM. Then output can be denoted as <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>o</mi><mo>=</mo><mi>L</mi><mi>L</mi><mi>M</mi><mo>(</mo><mi>q</mi><mo>,</mo><mi>Θ</mi><mo>)</mo></mrow><annotation>o=LLM(q, \Theta)</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">o</span><span class="mspace"></span><span class="mrel">=</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mord mathnormal">LL</span><span class="mord mathnormal">M</span><span class="mopen">(</span><span class="mord mathnormal">q</span><span class="mpunct">,</span><span class="mspace"></span><span class="mord">Θ</span><span class="mclose">)</span></span></span></span>. The task is fine-tune LLMs parameters and get the most of the reward.</p>
<p>The main issue that in order to tune the LLM you need to have some Loss function <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>L</mi><mo>(</mo><mi>o</mi><mo>,</mo><mover><mi>o</mi><mo>ˉ</mo></mover><mo>)</mo></mrow><annotation>L(o,\bar o)</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">L</span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mpunct">,</span><span class="mspace"></span><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="mord mathnormal">o</span></span><span><span class="pstrut"></span><span class="accent-body"><span class="mord">ˉ</span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span> where <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mover><mi>o</mi><mo>ˉ</mo></mover></mrow><annotation>\bar o</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="mord mathnormal">o</span></span><span><span class="pstrut"></span><span class="accent-body"><span class="mord">ˉ</span></span></span></span></span></span></span></span></span></span> is a correct answer. Then using Loss function you can calculate gradients and update model parameters. In the problem statement we have we do not have correct answers as most of the data is unlabelled. So instead we perform next trick.</p>
<p>We perform and action an <em>assume</em> that this action was correct. In this case loss <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>L</mi><mo>=</mo><mo>−</mo><mi>l</mi><mi>o</mi><mi>g</mi><mo>(</mo><mi>π</mi><mo>(</mo><mi>o</mi><mi>b</mi><mi>s</mi><mo>)</mo><mo>)</mo><mo>⋅</mo><mi>r</mi><mi>e</mi><mi>w</mi><mi>a</mi><mi>r</mi><mi>d</mi></mrow><annotation>L=-log(\pi(obs))\cdot reward</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">L</span><span class="mspace"></span><span class="mrel">=</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mord">−</span><span class="mord mathnormal">l</span><span class="mord mathnormal">o</span><span class="mord mathnormal">g</span><span class="mopen">(</span><span class="mord mathnormal">π</span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">b</span><span class="mord mathnormal">s</span><span class="mclose">))</span><span class="mspace"></span><span class="mbin">⋅</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mord mathnormal">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal">w</span><span class="mord mathnormal">a</span><span class="mord mathnormal">r</span><span class="mord mathnormal">d</span></span></span></span>. By default we calculate a gradient and perform gradient descent, reward in this case shows how big a step should be based of known correct answer. As we do not have a way to calculate it directly in our case, we introduce new function Advantage: <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi><mo>=</mo><mo>(</mo><mi>r</mi><mo>−</mo><mi>b</mi><mo>)</mo></mrow><annotation>A=(r-b)</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">A</span><span class="mspace"></span><span class="mrel">=</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mopen">(</span><span class="mord mathnormal">r</span><span class="mspace"></span><span class="mbin">−</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mord mathnormal">b</span><span class="mclose">)</span></span></span></span>, where <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>r</mi></mrow><annotation>r</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">r</span></span></span></span> is a post-action reward and <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>b</mi></mrow><annotation>b</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">b</span></span></span></span> is a baseline.</p>
<p>Reward <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>r</mi><mo>(</mo><mi>o</mi><mi>b</mi><mi>s</mi><mo>,</mo><mi>a</mi><mi>c</mi><mi>t</mi><mo>)</mo></mrow><annotation>r(obs,act)</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">r</span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">b</span><span class="mord mathnormal">s</span><span class="mpunct">,</span><span class="mspace"></span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span><span class="mord mathnormal">t</span><span class="mclose">)</span></span></span></span> is calculated via (1) some external reward estimation like complier with tests in the case of code, (2) some direct internal validation via unsupervised metrics or rule-based ones, (3) LLM as a judge like setting, where you use external LLM or even train one in parallel with this one. DeepSeek went with direct approach which is described in the point 7 in the previous section.</p>
<p>Baseline <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>b</mi></mrow><annotation>b</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">b</span></span></span></span> is calculated via value function, a regression that is pre-trained on the labeled data you have to answer the question “what will be an average reward for an action from a given state”.</p>
<p>Then loss can be written as</p>
<p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mtable><mtr><mtd class ="mtr-glue"></mtd><mtd><mstyle><mrow><mi>L</mi><mo>=</mo><mo>−</mo><mi>l</mi><mi>o</mi><mi>g</mi><mo>(</mo><mi>π</mi><mo>(</mo><mi>o</mi><mi>b</mi><mi>s</mi><mo>)</mo><mo>)</mo><mo>⋅</mo><mi>A</mi><mo>=</mo><mo>−</mo><mi>l</mi><mi>o</mi><mi>g</mi><mo>(</mo><mi>π</mi><mo>(</mo><mi>o</mi><mi>b</mi><mi>s</mi><mo>)</mo><mo>)</mo><mo>⋅</mo><mo>(</mo><mi>r</mi><mo>−</mo><mi>b</mi><mo>)</mo><mo>,</mo><mspace width="2.8453em"></mspace></mrow></mstyle></mtd><mtd class ="mtr-glue"></mtd><mtd class ="mml-eqn-num"></mtd></mtr></mtable><annotation>\begin{align}	
L=-log(\pi(obs))\cdot A =-log(\pi(obs))\cdot (r-b), \hspace{10mm} \tag{1}
\end{align}
</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="mord"><span class="mord mathnormal">L</span><span class="mspace"></span><span class="mrel">=</span><span class="mspace"></span><span class="mord">−</span><span class="mord mathnormal">l</span><span class="mord mathnormal">o</span><span class="mord mathnormal">g</span><span class="mopen">(</span><span class="mord mathnormal">π</span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">b</span><span class="mord mathnormal">s</span><span class="mclose">))</span><span class="mspace"></span><span class="mbin">⋅</span><span class="mspace"></span><span class="mord mathnormal">A</span><span class="mspace"></span><span class="mrel">=</span><span class="mspace"></span><span class="mord">−</span><span class="mord mathnormal">l</span><span class="mord mathnormal">o</span><span class="mord mathnormal">g</span><span class="mopen">(</span><span class="mord mathnormal">π</span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">b</span><span class="mord mathnormal">s</span><span class="mclose">))</span><span class="mspace"></span><span class="mbin">⋅</span><span class="mspace"></span><span class="mopen">(</span><span class="mord mathnormal">r</span><span class="mspace"></span><span class="mbin">−</span><span class="mspace"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mpunct">,</span><span class="mspace"></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span><span class="tag"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span><span class="mord text"><span class="mord">(</span><span class="mord"><span class="mord">1</span></span><span class="mord">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></p>
<p>where advantage <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>A</mi><mo>&gt;</mo><mn>0</mn></mrow><annotation>A&gt;0</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">A</span><span class="mspace"></span><span class="mrel">&gt;</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mord">0</span></span></span></span> when the action we perfromed is better than average expected and less than zero when vice versa.</p>
<p><strong>TRPO is a Trust Region Policy Optimization</strong> works the following way. You have a gradient, but you assume that it is dangerous to trust your gradient too much as it was produced by some random stochastic process (via working with concrete data samples). To incorporate that you modify your original loss by adding KL-divergence which basically says how different are 2 distributions:</p>
<p class="katex-block "><span class="katex-display"><span class="katex"><span class="katex-mathml"><math><semantics><mtable><mtr><mtd class ="mtr-glue"></mtd><mtd><mstyle><mrow><msub><mi>L</mi><mrow><mi>n</mi><mi>e</mi><mi>w</mi></mrow></msub><mo>=</mo><msub><mi>L</mi><mrow><mi>o</mi><mi>l</mi><mi>d</mi></mrow></msub><mo>+</mo><msub><mi>D</mi><mrow><mi>K</mi><mi>L</mi></mrow></msub><mo>(</mo><mi>π</mi><mo>,</mo><mover><mi>π</mi><mo>^</mo></mover><mo>)</mo><mi>.</mi><mspace width="2.8453em"></mspace></mrow></mstyle></mtd><mtd class ="mtr-glue"></mtd><mtd class ="mml-eqn-num"></mtd></mtr></mtable><annotation>\begin{align}	
L_{new}=L_{old}+D_{KL}(\pi, \hat \pi). \hspace{10mm}   \tag{2}
\end{align}
</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="mord"><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span><span class="mord mathnormal mtight">e</span><span class="mord mathnormal mtight">w</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mspace"></span><span class="mrel">=</span><span class="mspace"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">o</span><span class="mord mathnormal mtight">l</span><span class="mord mathnormal mtight">d</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mspace"></span><span class="mbin">+</span><span class="mspace"></span><span class="mord"><span class="mord mathnormal">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">K</span><span class="mord mathnormal mtight">L</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">π</span><span class="mpunct">,</span><span class="mspace"></span><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="mord mathnormal">π</span></span><span><span class="pstrut"></span><span class="accent-body"><span class="mord">^</span></span></span></span></span></span></span><span class="mclose">)</span><span class="mord">.</span><span class="mspace"></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span><span class="tag"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span><span class="mord text"><span class="mord">(</span><span class="mord"><span class="mord">2</span></span><span class="mord">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></p>
<p>Basically you are measuring how different your new policy in comparison to previous one you had and applying extra penalty on that, forcing gradient descent not to move too far away from the policy you had, which adds extra stability into the optimization process. Unfortunately TRPO is computationally intensive as in order to perform this estimation you need to calculate extra derivatives, make 2-nd order approximations, evaluate landscape and perform extra line search, so instead of it PPO approximation was developed.</p>
<p><strong>PPO is a Proximal Policy Optimization</strong> and has this complicated formula to work with, lets break it down:</p>
<p><img src="https://serokell.co/files/ai/aimvkol.image_(31).png" alt="aimvkol.image_(31).png"></p>
<p>(3)</p>
<p>In its core it has once again Policy multiplied by Advantage <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><mi>Θ</mi></msub><mo>(</mo><msub><mi>o</mi><mi>t</mi></msub><mi>∣</mi><mi>q</mi><mo>,</mo><msub><mi>o</mi><mrow><mo>&lt;</mo><mi>t</mi></mrow></msub><mo>)</mo><mo>⋅</mo><msub><mi>A</mi><mi>t</mi></msub></mrow><annotation>\pi_{\Theta}(o_t|q,o_{&lt;t})\cdot A_t</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">Θ</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">o</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mord">∣</span><span class="mord mathnormal">q</span><span class="mpunct">,</span><span class="mspace"></span><span class="mord"><span class="mord mathnormal">o</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mrel mtight">&lt;</span><span class="mord mathnormal mtight">t</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace"></span><span class="mbin">⋅</span><span class="mspace"></span></span><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span>. The <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><mrow><mi>o</mi><mi>l</mi><mi>d</mi></mrow></msub></mrow><annotation>\pi_{old}</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">o</span><span class="mord mathnormal mtight">l</span><span class="mord mathnormal mtight">d</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span> in  formula (3) is the instantiated model which produces outputs <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>q</mi></mrow><annotation>q</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">q</span></span></span></span> and <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>o</mi></mrow><annotation>o</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">o</span></span></span></span>, so it is just a number that can be computed directly from the current instance.  The  <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><mi>Θ</mi></msub></mrow><annotation>\pi_{\Theta}</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">Θ</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span> is the model you are optimizing gradients for, which will be used as old one on the next step. This results into the whole equation being the similar as before in (1) but with different pre-calculated constants and a bit different form. The main idea is that this helps to avoid the sampling bias of the current policy making weight of this part larger for policies that are rare under the old policy, as they are undersampled under it, they must be weighed stronger.</p>
<p>But, as we find more radical new policies, this drastically increases the first part and moves new policy too far away, resulting into the second term under the <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>m</mi><mi>i</mi><mi>n</mi></mrow><annotation>min</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">min</span></span></span></span> coming into play. This <span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>c</mi><mi>l</mi><mi>i</mi><mi>p</mi></mrow><annotation>clip</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord mathnormal">c</span><span class="mord mathnormal">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">p</span></span></span></span> function basically imitates simple cutting rule that works the same way as in TRPO, but without complicated calcluations.</p>
<p>The next thing they did they applied the same mechanism that was showed in (2), but instead of using some heavy calculations to obtain KL-divergence they made in a sense similar term:</p>
<p><img src="https://serokell.co/files/ac/aclh92m.image_(32).png" alt="aclh92m.image_(32).png"> (4)</p>
<p>The <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><mrow><mi>r</mi><mi>e</mi><mi>f</mi></mrow></msub></mrow><annotation>\pi_{ref}</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">r</span><span class="mord mathnormal mtight">e</span><span class="mord mathnormal mtight">f</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span> here is the reference model, which is usually the initial SFT model they had at the initialization of the whole optimization process. The main idea is that while we want to perform RL optimization, we still assume that initial model already had somewhat good representation of the world, and we do not want to move to far away from that. Although it is not exactly the same as KL-divergence used in TRPO, it still gives similar results.</p>
<p>Next, they decided to move from direct advantage to more computationally effective approach. They made groups of outputs by direct sampling and from the old policy <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><msub><mi>Θ</mi><mrow><mi>o</mi><mi>l</mi><mi>d</mi></mrow></msub></msub></mrow><annotation>\pi_{\Theta_{old}}</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mtight">Θ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">o</span><span class="mord mathnormal mtight">l</span><span class="mord mathnormal mtight">d</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span> and optimized policy model <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mi>π</mi><mi>Θ</mi></msub></mrow><annotation>\pi_{\Theta}</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord mathnormal">π</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">Θ</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span> by maximizing this objective:</p>
<p><img src="https://serokell.co/files/ae/aepfg81.image_(33).png" alt="aepfg81.image_(33).png"></p>
<p>(5)</p>
<p>It is the same as before in (3), but with (4) added in the end, group sampling instead object sampling and new advantage function <span class="katex"><span class="katex-mathml"><math><semantics><mrow><msub><mover><mi>A</mi><mo>^</mo></mover><mrow><mi>i</mi><mo>,</mo><mi>t</mi></mrow></msub></mrow><annotation>\hat A_{i,t}</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut"></span><span class="mord"><span class="mord accent"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="mord mathnormal">A</span></span><span><span class="pstrut"></span><span class="accent-body"><span class="mord">^</span></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span><span class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight">t</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist"><span></span></span></span></span></span></span></span></span></span> calculated based on relative rewards of the outputs inside each group only. <strong>This group-relative advantage calculation helps to exclude value model, which usually required to run secondary instance of LLM model and thus cut a lot of computational resources</strong>.</p>
<p>If you want some extra comments on how and why it works you can try to read the original R1 paper. But the main idea of the benefits we got was shown in this image with transition from PPO to GRPO.</p>
<p><img src="https://serokell.co/files/ae/aeit39q.image_(34).png" alt="aeit39q.image_(34).png"></p>
<p>To sum up, as a result of this transition:</p>
<ul>
<li>Value model which was computational heavy is excluded.</li>
<li>Instead of estimating average possible reward with value model we just sample several outputs, evaluate them with reward model and use average reward as our “value” in older terms.</li>
<li>For a reward model we use simple rule-based system which was described in point 7 of previous section. This also reduces computational costs.</li>
<li>Changed heavy KL-divergence calculation with some light-weighted approximation.</li>
</ul>
<p>As a result we only need to make some extra sampling, apply light-weighted reward model to get average reward and apply the same procedure as it was in PPO with some extra tweaks while nearly halving all other much heavier calculations.</p>
<h1 id="outro" tabindex="-1">Outro</h1>
<p>Hopefully this blogpost gave you a better understanding of the foundations of the DeepSeek innovations. I tried to collect everything you need to know about it and tackle every rumour we had so far. You can share it with any person who gives provoking and unsupported claims about various aspects of the R1 model, hopefully this helps.</p>
<h1 id="sources" tabindex="-1">Sources</h1>
<p>More detailed analyses in visual form:</p>
<ul>
<li>DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models by Yannic Kilcher <a href="https://youtu.be/bAWV_yrqx4w">https://youtu.be/bAWV_yrqx4w</a></li>
<li>DeepSeek-R1 Paper Explaned — A New RL LLMs Era in AI? by AI Papers Academy <a href="https://www.youtube.com/watch?v=DCqqCLlsIBU">https://www.youtube.com/watch?v=DCqqCLlsIBU</a></li>
<li>DeepSeek, China, OpenAI, NVIDIA, xAI, TSMC, Stargate, and AI Megaclusters | Lex Fridman Podcast 459 <a href="https://www.youtube.com/watch?v=_1f-o0nqpEI">https://www.youtube.com/watch?v=_1f-o0nqpEI</a></li>
</ul>
<p>For the advanced readers:</p>
<ul>
<li>Proximal Policy Optimization Algorithms <a href="https://arxiv.org/abs/1707.06347">https://arxiv.org/abs/1707.06347</a></li>
<li>DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models <a href="https://arxiv.org/abs/2402.03300">https://arxiv.org/abs/2402.03300</a></li>
<li>DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning <a href="https://arxiv.org/abs/2501.12948">https://arxiv.org/abs/2501.12948</a></li>
</ul>
</p>]]></content:encoded>
            <author>hi+ivan-smetannikov@serokell.co (Ivan Smetannikov)</author>
            <enclosure url="https://serokell.co/files/at/thumb.at84guh.normal.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>