{"@attributes":{"version":"2.0"},"channel":{"title":"Posts on Ethan Woodbury","link":"https:\/\/ewoodbury.com\/","description":"Recent content in Posts on Ethan Woodbury","generator":"Hugo -- gohugo.io","language":"en-US","managingEditor":"ethwoodbury@gmail.com (Ethan Woodbury)","webMaster":"ethwoodbury@gmail.com (Ethan Woodbury)","copyright":"Copyright \u00a9 2025, Ethan Woodbury","lastBuildDate":"Mon, 12 Oct 2026 00:00:00 +0000","item":[{"title":"Vectorized data processing, explained","link":"https:\/\/ewoodbury.com\/vectorized-data\/","pubDate":"Mon, 12 Oct 2026 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/vectorized-data\/","description":"<p>Vectorization is the technique of running a tight loop over a column of values, typically a few thousand values at a time. It&rsquo;s used by ClickHouse, DuckDB, Numpy, Datafusion, and all modern analytical processing engines.<\/p>\n<p>This contrasts with how most backend data systems work. Postgres and MySQL walk one record at a time. Spark SQL also steps one record at a time inside a stage. The values are raw bytes, not an object each. The Parquet reader is the part that is actually columnar, and it turns those columns back into rows for the rest of the plan. For analytical use cases, where filters and aggregations on large columns are common, vectorization has become the standard because it is so much faster.<\/p>\n<p>Vectorization works in any language; it doesn&rsquo;t need a native language like C++ or Rust (a common misconception). In fact, this post builds the entire engine in pure Scala to demonstrate; the principles apply no matter what language is in use.<\/p>\n<hr>\n<h2 id=\"1-row-based-engine\">1. Row-based Engine<\/h2>\n<p>Let&rsquo;s start with a basic query operation with a <code>filter<\/code> and <code>map<\/code> stage in Scala:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span>values<span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#ae81ff\">0<\/span><span style=\"color:#f92672\">).<\/span>map<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#f92672\">*<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">).<\/span>collect<span style=\"color:#f92672\">()<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>To implement this in a basic query engine, we define operators for each of the <code>filter<\/code> and the <code>map<\/code>, each of which take a function <code>f<\/code> as a parameter. Each operator here uses a simple iterator to walk over each element in <code>values<\/code>.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> filter<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span><span style=\"color:#f92672\">](<\/span>f<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">A<\/span> <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#a6e22e\">Boolean<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">A<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">SingleOpStage<\/span> <span style=\"color:#f92672\">{<\/span> p <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> it <span style=\"color:#66d9ef\">=<\/span> p<span style=\"color:#f92672\">.<\/span>data<span style=\"color:#f92672\">.<\/span>iterator<span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Partition<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">IterUtil<\/span><span style=\"color:#f92672\">.<\/span>iterableOf<span style=\"color:#f92672\">(<\/span>it<span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> map<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">](<\/span>f<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">A<\/span> <span style=\"color:#f92672\">=&gt;<\/span> B<span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">SingleOpStage<\/span> <span style=\"color:#f92672\">{<\/span> p <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> it <span style=\"color:#66d9ef\">=<\/span> p<span style=\"color:#f92672\">.<\/span>data<span style=\"color:#f92672\">.<\/span>iterator<span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Partition<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">IterUtil<\/span><span style=\"color:#f92672\">.<\/span>iterableOf<span style=\"color:#f92672\">(<\/span>it<span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This query walks over all elements and calls the <code>filter<\/code> function on each, allocates memory, and creates a new <code>Partition<\/code> data structure for that result. It then does the same with the <code>map<\/code> function on each element, again allocating a new <code>Partition<\/code> for the result. That&rsquo;s a lot of steps!<\/p>\n<p>For this case, the data is not physically located together in memory. Since each int is its own object, the next value is a pointer chase away rather than at the next 4 bytes. The CPU loads each value into memory, processes it, reads the pointer for the next value, then goes all the way back to memory to fetch it.<\/p>\n<p>Another potential slowdown is that of type conversions, or &ldquo;boxing&rdquo; as it&rsquo;s known in the JVM. The simple row-based Scala engine converts from the <code>java.lang.Integer<\/code> to a raw 4-byte int before doing the comparison, then boxes the result back into <code>java.lang.Integer<\/code>. This is true (to some extent) for all high-level languages like Scala which manage types.<\/p>\n<p>Due to both one-by-one processing and boxing, the row-based engine on the 1 million element list is pretty slow! The row engine&rsquo;s <code>collect<\/code> of the same query took about 195 ms and allocated about 84 MB, on 4 threads. A million raw ints are 4 MB, so that is about 20\u00d7 the data.<\/p>\n<div class=\"mermaid\">flowchart TD\nsubgraph filt[\"Filter loop, x 1,000,000\"]\nf1[\"Chase next pointer\"] --> f2[\"Unbox the Int\"]\nf2 --> f3{\"> 0?\"}\nf3 -->|no| f1\nf3 -->|yes| f4[\"Allocate list node\"]\nf4 --> f1\nend\nmid1[\"New list, in memory\"]\nsubgraph mapl[\"Map loop, x 500,000\"]\nm1[\"Chase next pointer\"] --> m2[\"Unbox the Int\"]\nm2 --> m3[\"Multiply by 2\"]\nm3 --> m4[\"Allocate new Int and list node\"]\nm4 --> m1\nend\nfilt --> mid1 --> mapl\nstyle filt fill:#fff3bf,stroke:#e6b800\nstyle mapl fill:#fff3bf,stroke:#e6b800\n<\/div>\n<p><img src=\"step-row.png\" alt=\"Row-Based, 195 ns\/integer. Filter + map, 1 million integers.\"><\/p>\n<h2 id=\"2-vectorized-engine\">2. Vectorized Engine<\/h2>\n<p>The key principle to understand for vectorization is that of contiguous data. When the engine sets the data to be processed as an entire column (or vector) at once, the next value is always 4 bytes away from the current value.<\/p>\n<p>The column is stored in a contiguous location of memory addresses: in our case the 1 million ints take 4MB of contiguous addresses.<\/p>\n<p>In the engine, that column is an <code>Array[Int]<\/code>, and the kernels are plain <code>while<\/code> loops over it. Each step handles one int. No SIMD instruction is required, and the filter stays one int wide even when HotSpot&rsquo;s auto-vectorizer is on. This is the unfused columnar engine the earlier Scarlet timings measured: two passes, one value at a time.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> filterInt32<span style=\"color:#f92672\">(<\/span>in<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">],<\/span> <span style=\"color:#a6e22e\">Int<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> out <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">](<\/span>in<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">var<\/span> written <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">var<\/span> row <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">while<\/span> <span style=\"color:#f92672\">(<\/span>row <span style=\"color:#f92672\">&lt;<\/span> in<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>in<span style=\"color:#f92672\">(<\/span>row<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#ae81ff\">0<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> out<span style=\"color:#f92672\">(<\/span>written<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> in<span style=\"color:#f92672\">(<\/span>row<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> written <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> row <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">(<\/span>out<span style=\"color:#f92672\">,<\/span> written<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> projectInt32<span style=\"color:#f92672\">(<\/span>in<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">],<\/span> length<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> out <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">](<\/span>length<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">var<\/span> row <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">while<\/span> <span style=\"color:#f92672\">(<\/span>row <span style=\"color:#f92672\">&lt;<\/span> length<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> out<span style=\"color:#f92672\">(<\/span>row<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> in<span style=\"color:#f92672\">(<\/span>row<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">*<\/span> <span style=\"color:#ae81ff\">2<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> row <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> out\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> <span style=\"color:#f92672\">(<\/span>filtered<span style=\"color:#f92672\">,<\/span> survivors<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> filterInt32<span style=\"color:#f92672\">(<\/span>column<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> projected <span style=\"color:#66d9ef\">=<\/span> projectInt32<span style=\"color:#f92672\">(<\/span>filtered<span style=\"color:#f92672\">,<\/span> survivors<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>When the CPU starts processing the data in the column, it knows that the next value is always 4 bytes ahead of the current one. Therefore, it pulls an entire chunk of data, 64 bytes or 16 ints, from memory into L1 cache, before the CPU processing loop even asks for it. Then the CPU always has the next value available in L1, whereas in the row-based approach, the CPU needs to read the pointer then pull the next value from memory, for every single value.<\/p>\n<p>Recall the standard data storage on a CPU (sizes for an Intel i7-8550U):<\/p>\n<ol>\n<li><strong>CPU register<\/strong>: directly on the CPU, accessible in 1 CPU clock cycle, for bytes being processed<\/li>\n<li><strong>L1 cache<\/strong>: 32KB, accessible in about 4 clock cycles<\/li>\n<li><strong>L2 cache<\/strong>: 256KB, accessible in 10-15 clock cycles<\/li>\n<li><strong>L3 cache<\/strong>: 8MB, accessible in ~20-40 clock cycles<\/li>\n<li><strong>Memory (RAM)<\/strong>: 12GB, accessible in ~100-400 clock cycles<\/li>\n<\/ol>\n<p>In the 1-million int example, the data to process is always ready in L3 or L1 cache, so the CPU is fully saturated and speeds through the 1 million ints for the filter step, writing the filter result back to memory. Then it speeds through the map step as well. The data fetch from RAM to L3 is usually the bottleneck in reality.<\/p>\n<div class=\"mermaid\">flowchart TD\nsubgraph filt[\"Filter loop, x 1,000,000\"]\nf1[\"Load next int\"] --> f2{\"> 0?\"}\nf2 -->|no| f1\nf2 -->|yes| f3[\"Store survivor\"]\nf3 --> f1\nend\nmid[\"Column, in memory\"]\nsubgraph proj[\"Map loop, x 500,000\"]\np1[\"Load filtered int\"] --> p2[\"Multiply by 2\"]\np2 --> p3[\"Store result\"]\np3 --> p1\nend\nfilt --> mid --> proj\nstyle filt fill:#fff3bf,stroke:#e6b800\nstyle proj fill:#fff3bf,stroke:#e6b800\n<\/div>\n<p><img src=\"step-column.png\" alt=\"Row-Based 195 ns\/integer, Vectorized 11.8 ns\/integer.\"><\/p>\n<h2 id=\"3-vectorized-engine-with-fusing\">3. Vectorized Engine with Fusing<\/h2>\n<p>In the previous example, the engine executes two CPU instructions on each element, so each value leaves the CPU in between the oerations. The data gets pulled from memory to L3 to L1 to CPU for the first filter, only to be pushed back to L3 then L1 then CPU again for the mutiply. With data movement being the bottleneck, the overall query is limited by moving values between cache levels up to 2 million times, for only 1 million values.<\/p>\n<p>This can be optimized beforehand in the planning stage, by fusing the CPU operations into a single kernel instead. The CPU can run both the filter and the multiply step while the data is still in the register, so there is no intermediate array and no data movement.<\/p>\n<p>The planner matches the chain outside the loop and picks one pipeline. For this query that is <code>Int32Pipeline.FP<\/code>, and <code>inline<\/code> pastes both functions into its loop. One pass, one output column.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">Filter<\/span><span style=\"color:#f92672\">(<\/span>predicate<span style=\"color:#f92672\">),<\/span> <span style=\"color:#a6e22e\">Project<\/span><span style=\"color:#f92672\">(<\/span>mapper<span style=\"color:#f92672\">))<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Int32Pipeline<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">FP<\/span><span style=\"color:#f92672\">(<\/span>batch<span style=\"color:#f92672\">,<\/span> predicate<span style=\"color:#f92672\">,<\/span> mapper<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">var<\/span> written <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">var<\/span> row <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">while<\/span> <span style=\"color:#f92672\">(<\/span>row <span style=\"color:#f92672\">&lt;<\/span> live<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> value <span style=\"color:#66d9ef\">=<\/span> in<span style=\"color:#f92672\">(<\/span>row<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>predicate<span style=\"color:#f92672\">(<\/span>value<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> out<span style=\"color:#f92672\">(<\/span>written<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> mapper<span style=\"color:#f92672\">(<\/span>value<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> written <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> row <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Note that fusing is separate from vectorization, and it&rsquo;s been a common operation in query engines for a long time, including in Spark. With vectorization and fusing together though, for large column-heavy workloads, engines see massive performance benefits.<\/p>\n<div class=\"mermaid\">flowchart TD\nsubgraph kern[\"One loop, x 1,000,000\"]\na[\"Load int into register\"] --> b{\"> 0?\"}\nb -->|no| a\nb -->|yes| c[\"Multiply by 2\"]\nc --> d[\"Store once\"]\nd --> a\nend\nstyle kern fill:#fff3bf,stroke:#e6b800\n<\/div>\n<p><img src=\"step-fused.png\" alt=\"Row-Based 195 ns\/integer, Vectorized 11.8, Vectorized + Fused 8.9.\"><\/p>\n<h2 id=\"4-simd\">4. SIMD<\/h2>\n<p>You might have immediately thought about SIMD (Single Instruction Multiple Data), which is the more well-known form of vectorized optimizations. However we leave this until the end, to illustrate the significant gains from the simpler column loop, and from fusion, first. SIMD lives one level even deeper, within the CPU instructions themselves.<\/p>\n<p>The core idea here is exactly as it sounds: a single CPU instruction running over multiple values. In this example, the operation <code>out[i] = in[i] * 2<\/code> becomes a single AVX2 instruction operating on 8 ints (32 bytes) at once. All 8 ints are loaded into adjacent lanes as a contiguous block, the CPU shifts them all, then all 8 results are stored together.<\/p>\n<p>For a dense <code>out(i) = in(i) * 2<\/code>, a plain <code>while<\/code> over an <code>Array[Int]<\/code> is not only vectorized, it is also set to automatically use SIMD on modern JVM compilers such as HotSpot. That pass is SuperWord Level Parallelism, on by default, and it can be enabled or disabled as compiler flags:<\/p>\n<pre tabindex=\"0\"><code>java -XX:+UseSuperWord ... # on, default\njava -XX:-UseSuperWord ... # off, scalar\n<\/code><\/pre><p>The filter and fused loops above stay one int per step with the flag either way. The multiply is the loop that changes. A Scala engine can also skip the auto-vectorizer and call the instruction explicitly, using <code>jdk.incubator.vector<\/code>. Here <code>SPECIES_256<\/code> represents 8 ints (256 bits) processed in parallel across lanes:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> jdk.incubator.vector.<span style=\"color:#f92672\">{<\/span><span style=\"color:#a6e22e\">IntVector<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">VectorOperators<\/span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> explicitMul<span style=\"color:#f92672\">(<\/span>in<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">],<\/span> out<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Array<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> species <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">IntVector<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">SPECIES_256<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> lanes <span style=\"color:#66d9ef\">=<\/span> species<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#f92672\">()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">var<\/span> i <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> bound <span style=\"color:#66d9ef\">=<\/span> species<span style=\"color:#f92672\">.<\/span>loopBound<span style=\"color:#f92672\">(<\/span>in<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">while<\/span> i <span style=\"color:#f92672\">&lt;<\/span> bound <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> v <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">IntVector<\/span><span style=\"color:#f92672\">.<\/span>fromArray<span style=\"color:#f92672\">(<\/span>species<span style=\"color:#f92672\">,<\/span> in<span style=\"color:#f92672\">,<\/span> i<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> v<span style=\"color:#f92672\">.<\/span>lanewise<span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">VectorOperators<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">LSHL<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">).<\/span>intoArray<span style=\"color:#f92672\">(<\/span>out<span style=\"color:#f92672\">,<\/span> i<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> i <span style=\"color:#f92672\">+=<\/span> lanes\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">while<\/span> i <span style=\"color:#f92672\">&lt;<\/span> in<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#66d9ef\">do<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> out<span style=\"color:#f92672\">(<\/span>i<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> in<span style=\"color:#f92672\">(<\/span>i<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">*<\/span> <span style=\"color:#ae81ff\">2<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> i <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>The 8th-gen i7 used for testing doesn&rsquo;t support the newer instruction set (AVX-512) which would let it use SIMD on the filter and map together due to needing to drop the filtered values, so this test only measured the impact of SIMD onthe map. SIMD showed some improvement due to the faster CPU operations, but since this data is large enough to not fit entirely into cache, the operation is still bottlenecked by the fetch from memory which is not impacted by SIMD.<\/p>\n<div class=\"mermaid\">flowchart TD\nsubgraph kern[\"Dense multiply, x 125,000\"]\na[\"Load 8 ints\"] --> b[\"Shift all 8\"]\nb --> c[\"Store 8 ints\"]\nc --> a\nend\nstyle kern fill:#fff3bf,stroke:#e6b800\n<\/div>\n<p><img src=\"step-simd.png\" alt=\"Map only, 8 million integers. Scalar 0.657 ns\/integer, SIMD 0.585 ns\/integer.\"><\/p>\n<h2 id=\"on-languages\">On Languages<\/h2>\n<p>I wrote these examples here in Scala, rather than a lower-level language, to illustrate the point that vectorization itself is language agnostic. In fact, my entire for-fun processing engine project, <a href=\"https:\/\/github.com\/ewoodbury\/scarlet\" target=\"_blank\" rel=\"noopener\">Scarlet<\/a>, is in Scala yet still beats Apache Spark easily thanks in large part due to vectorization.<\/p>\n<p>Language choice has no effect once code is compiled down to the equivalent instructions within the processing loop. Of course, we saw that the JVM provides auto-vectorization and other optimizations during the compile step itself, which is in contrast to interpreted languages like Python which are stuck with executing operations as specified (although this is starting to change with the in-progress <a href=\"https:\/\/peps.python.org\/pep-0836\/\" target=\"_blank\" rel=\"noopener\">Python JIT<\/a>).<\/p>\n<p>Still, low-level languages like C++ and Rust provide more direct memory control, specifically in being able to avoid the boxing and unboxing conversion cost of values, whereas JVM languages pay the boxing tax at the beginning and end of the operation when converting raw bytes. They can also specify the exact instruction: for the filter step, a C++\/Rust could the AVX-512 compress on a CPU that has it, instead of leaving that choice to the JIT compiler.<\/p>"},{"title":"Reviving a 9-year-old laptop with Linux and new hardware","link":"https:\/\/ewoodbury.com\/laptop-revival\/","pubDate":"Mon, 21 Sep 2026 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/laptop-revival\/","description":"<p>I have a 2017 HP Pavilion 15-cc6xx, with an Intel i7-8550U (8th-gen) and 12GB RAM. This machine has been through a lot: it&rsquo;s what got me through my last 3 years of my Chemical Engineering degree, plus my first internship and job searches. I had replaced it with a Macbook Pro M1 back in 2022, so the HP has been sitting collecting dust for 5 years now!<\/p>\n<p>I had actually tried out Linux before on a very old laptop while back in high school; it didn&rsquo;t go great as the hardware was even worse, I set it as dual-boot to keep Windows which caused problems, but most of all I just didn&rsquo;t keep at it long enough to get it to a great state.<\/p>\n<p>This time around, I did a bit more research upfront, but most importantly, coding agents exist now! I do a lot of compute cluster and infra work at work now, and agents have changed the game from Googling and searching through forums to simply asking the agent. Linux has a similar problem profile of fiddly config knobs, so with agents it seemed like a great new opportunity to try it out. It led me to some hardware upgrades as well. Spoiler - it went great!<\/p>\n<p>Final results:<\/p>\n<ul>\n<li>45% shorter app startup times<\/li>\n<li>61% faster browser performance<\/li>\n<li>~3x faster sequential read\/write speeds, ~49x faster random read latency<\/li>\n<li>~3x longer battery life<\/li>\n<\/ul>\n<hr>\n<h2 id=\"linux-install-cachyos\">Linux Install: CachyOS<\/h2>\n<p>Linux distros are a persistently hot topic, and I don&rsquo;t have much new to add to that conversation. I chose CachyOS simply because it&rsquo;s built to be resource-efficient, and because being Arch-based, it seemed to have a bit more room for customizability. The other distro I closely considered was NixOS, for its ideas around reproducibility, declarative builds, and basically the whole idea of defining an &ldquo;OS as code&rdquo; (OSaC?), but alas it will wait for another day.<\/p>\n<p>The CachyOS install itself was mostly uneventful. I opted to do a full wipe of the Windows installation for simplicity. The steps were standard:<\/p>\n<ul>\n<li>Backed up remaining files I wanted to keep from Windows<\/li>\n<li>Downloaded the CachyOS ISO onto a spare 32GB USB drive<\/li>\n<li>Booted the HP into the BIOS menu<\/li>\n<li>Disabled HP Secure Boot, then rebooted into BIOS again (CachyOS seemed to not support Secure Boot at least for my HP)<\/li>\n<li>Selected to boot CachyOS from the USB<\/li>\n<li>Once booted into CachyOS in RAM (screenshot below), followed the installer to wipe the disk fully and installed CachyOS into there<\/li>\n<\/ul>\n<p><img src=\"cachyos-welcome.png\" alt=\"CachyOS Hello welcome screen in the live USB install environment\"><\/p>\n<ul>\n<li>Rebooted one last time, to finally get into the fresh disk install of CachyOS with my selected desktop environment (Xfce)<\/li>\n<\/ul>\n<p><img src=\"cachyos-desktop.png\" alt=\"The installed CachyOS Xfce desktop\"><\/p>\n<p>I had a couple hiccups to figure it all out, including identifying Secure Boot as the reboot issue and getting the disk partitions right in the installer. But I was able to have an LLM walk me through the main steps and help me get unstuck at each stage.<\/p>\n<p>In the CachyOS installer, it also prompts to pick a desktop environment. After a bit more research I chose <a href=\"https:\/\/www.xfce.org\/\" target=\"_blank\" rel=\"noopener\">Xfce<\/a>, again just for its focus on simplicity and resource efficiency. I&rsquo;ve come to really liking it; it beats Windows by miles (of course) but I think I even prefer its overall experience over MacOS.<\/p>\n<h2 id=\"linux-configuration-and-the-frozen-screen-of-death\">Linux Configuration and the Frozen Screen of Death<\/h2>\n<p>One issue started popping up as I used CachyOS: every couple of hours, the screen would freeze on its last frame and all input would go dead. It was a wedge rather than a regular OS crash: no OOM, no thermal event in the logs; the journal just stopped mid-line. The kernel never even got far enough to panic, and the only recovery from the freeze was a hard power-off.<\/p>\n<p>A few things I tried:<\/p>\n<ul>\n<li>Switched to the LTS kernel - froze anyway, ruling out a regression in the CachyOS-tuned kernel<\/li>\n<li>Ran two full memtest86+ passes with zero errors - my mismatched 4GB + 8GB RAM sticks were the top suspect, but they were cleared<\/li>\n<li>Disabled WiFi power saving - froze anyway<\/li>\n<li>Checked disk health with SMART - clean<\/li>\n<\/ul>\n<p>The real break was circumstantial: I (with Grok help) noticed several freezes hit seconds after the screensaver unlocked or the display woke up. Following that thread, I found a kernel worker, <code>kworker\/u33:3+i915_flip<\/code>, stuck in uninterruptible sleep (D state) for 20+ minutes with no corresponding error in the kernel log. That worker exists to service display page-flips, which pointed to the i915 graphics driver.<\/p>\n<p>Disabling Panel Self-Refresh (<code>i915.enable_psr=0<\/code>, an apparently flaky feature on this gen of Intel iGPUs) and display C-states (<code>i915.enable_dc=0<\/code>) on the kernel command line was the first big win, as it dropped the freeze frequency to every few days.<\/p>\n<p>The final fix ended up being simple: turning off the Xfce compositor with <code>xfconf-query -c xfwm4 -p \/general\/use_compositing -s false<\/code>. The frozen <code>i915_flip<\/code> worker disappeared, rendering stayed hardware-accelerated, and there hasn&rsquo;t been a display freeze since. The compositor&rsquo;s page-flip path simply seems to hit some edge case and cause the worker wedge on this old GPU.<\/p>\n<p>What I&rsquo;d call out here is the lingering edge cases in setting up Linux on an old laptop; this kind of issue would have killed the whole project if I were lazier or didn&rsquo;t have coding agents. The only reason I knew to try any of these potential fixes was that I could simply ping Grok to check my system logs and propose fixes, and that&rsquo;s what ultimately suggested the compositor switch. I hope coding agents might help Linux and tinkering gain popularity.<\/p>\n<h2 id=\"ssd-upgrade\">SSD Upgrade<\/h2>\n<p>As I was debugging the frozen screen issue, and as I was just asking Grok\/other models about the Linux install, one thing that came up with the slow disk speeds. It turns out this laptop was on a pretty budget HDD this entire time, and disk read speed was a huge bottleneck for basically every operation: boot times, app startup, file loading, etc. Memory swap was basically impossible, which explained why multitasking would always feel so sluggish back on Windows during my college days.<\/p>\n<p>The coding agent figured out there was a spare, empty SSD slot in the laptop, and it measured the disk read speeds and used that to project that adding a basic M.2 SSD would speed up sequential reads by ~8-10x, and random reads (latency) by even more. HDD vs. SSD performance is well-known, and every new mid and upper-tier laptop has come with an SSD for many years now. It sounds silly to be spending money upgrading a 9-year-old laptop, but I had already spent the time reinstalling the OS and was eager to make it daily driver-worthy, so I pulled the trigger on a <a href=\"https:\/\/www.amazon.com\/dp\/B08GJDHMPB\" target=\"_blank\" rel=\"noopener\">512GB Timetec M.2 SATA 2280 SSD<\/a> from Amazon for $72.<\/p>\n<p>Once it arrived, I took off the bottom cover and installed the SSD. This was pretty easy; I&rsquo;ve done the same with replacing the SSD in my Steam Deck, so I did the same here (there are many online guides and videos).<\/p>\n<p>After reboot, the OS was still installed only on the HDD. I asked Grok Build to find the new SSD and help me get the OS set up there, and it was able to figure it all out! From what I understand, it created a new drive object within the OS on the SSD, copied the entire OS and filesystem over from the HDD, and then it set the startup settings to boot onto the SSD instead. The boot settings did not work at first: it booted back onto the OS on HDD a couple times, then it landed on the SSD, then back on the HDD. Finally it was able to figure out the configs and get the SSD set permanently.<\/p>\n<div class=\"image-pair\">\n<img src=\"ssd-before.jpg\" alt=\"Empty M.2 slot in the HP Pavilion\" \/>\n<img src=\"ssd-after.jpg\" alt=\"Timetec M.2 SATA SSD installed\" \/>\n<\/div>\n<p>I was shocked at how much snappier everything was on SSD. Ghostty started in one second! Firefox in under two! I constantly using Alt-Tab to switch across windows, and Ctrl + Space to open App Finder, and those both seem to literally load instantly which makes a massive difference in OS feel. It blows my mind that I just accepted the slow HDD performance for so long, using it for Matlab projects to Python data analysis to <a href=\"https:\/\/www.aspentech.com\/en\/products\/engineering\/aspen-hysys\" target=\"_blank\" rel=\"noopener\">Aspen HYSYS<\/a> (heavyweight modelling software).<\/p>\n<p>The HDD is still installed and holds the stale OS copy. I plan to wipe the OS there and then use the HDD as bulk storage, as it&rsquo;s a full 1TB of good functional storage, but I haven&rsquo;t gotten around to that yet.<\/p>\n<p>For actual performance benchmarks, I did have the foresight to record some numbers before and after the upgrade - see <a href=\"#performance-summary\">Performance Summary<\/a> below.<\/p>\n<h2 id=\"battery-upgrade\">Battery Upgrade<\/h2>\n<p>The other hardware issue I figured out from Grok Build was the battery. I had always accepted that this HP laptop had terrible battery life; I broght the charging brick everywhere during school, and I memorized dozens of study spots across campus with outlet access. Anything more than a 1-2 hour study period and I knew the laptop battery wouldn&rsquo;t make it.<\/p>\n<p>The agent started out measuring average power draw around 5W, with heavy use going up to ~8-10W. With the design energy of 42Wh, and assuming a conservative 50-60% battery state of health after many years, that should be giving around 3 hours of mixed use battery life. But still, I was finding myself lucky to get even 1.5 hours, which measured up to ~15-20Wh of energy. It turns out the design capacity was being reported to the firmware as only 15.5Wh! Even on an old battery, this design capacity should show 42Wh. I&rsquo;m still not 100% sure if the original battery was counterfeit (seems unlikely given the laptop was ordered from Costco), or if the battery just had major degradation and somehow had the capacity reproting reset maybe due to the OS switch. But it was clear a battery update was due if I wanted the laptop to be portable at all.<\/p>\n<p>Again, I ordered a new replacement online: a TF03XL part number pouch cell battery, and I chose one sold by brand GHU for $33, since it seemed to have good reviews and a legit website. It came in, I cracked open the cover again, and I replaced the battery itself.<\/p>\n<p><img src=\"battery-installed.jpg\" alt=\"New GHU TF03XL battery installed\"><\/p>\n<p>After reboot, it got stuck on a BIOS <code>Unauthenticated<\/code> error as I had to re-disable secure boot, then it booted healthy. I did a full capacity test from 100% to 10%, and once again this hardware upgrade did not disappoint: it gets 97% of the designed 42Wh energy. It&rsquo;s getting an extremely healthy 6+ hours of mixed use battery life, more than double the previous life. Again, an amazing upgrade compared to before, and I kicking myself for not trying this when I was actually using this laptop all the time in college!<\/p>\n<h2 id=\"performance-summary\">Performance Summary<\/h2>\n<p>All before\/after numbers measured with identical methodology (fio direct I\/O, <code>systemd-analyze<\/code>, hyperfine 10 runs), on the same OS and kernel.<\/p>\n<p><strong>Storage (fio, direct I\/O):<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th style=\"text-align: right\">HDD (Seagate 1TB SMR)<\/th>\n<th style=\"text-align: right\">SSD (Timetec M.2 SATA)<\/th>\n<th style=\"text-align: right\">Change<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Sequential write, 1M blocks<\/td>\n<td style=\"text-align: right\">80.2 MiB\/s<\/td>\n<td style=\"text-align: right\">248 MiB\/s<\/td>\n<td style=\"text-align: right\">3.1x<\/td>\n<\/tr>\n<tr>\n<td>Sequential read, 1M blocks<\/td>\n<td style=\"text-align: right\">112 MiB\/s<\/td>\n<td style=\"text-align: right\">307 MiB\/s<\/td>\n<td style=\"text-align: right\">2.7x<\/td>\n<\/tr>\n<tr>\n<td>4K random read, QD1<\/td>\n<td style=\"text-align: right\">96 IOPS<\/td>\n<td style=\"text-align: right\">4,658 IOPS<\/td>\n<td style=\"text-align: right\">48x<\/td>\n<\/tr>\n<tr>\n<td>4K random write, QD1<\/td>\n<td style=\"text-align: right\">102 IOPS<\/td>\n<td style=\"text-align: right\">4,629 IOPS<\/td>\n<td style=\"text-align: right\">45x<\/td>\n<\/tr>\n<tr>\n<td>Average op latency<\/td>\n<td style=\"text-align: right\">~10.3 ms<\/td>\n<td style=\"text-align: right\">~0.21 ms<\/td>\n<td style=\"text-align: right\">~49x<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Boot time (systemd-analyze):<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Stage<\/th>\n<th style=\"text-align: right\">HDD<\/th>\n<th style=\"text-align: right\">SSD<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Userspace<\/td>\n<td style=\"text-align: right\">18.3 s<\/td>\n<td style=\"text-align: right\">10.9 s<\/td>\n<\/tr>\n<tr>\n<td>To graphical target<\/td>\n<td style=\"text-align: right\">16.4 s<\/td>\n<td style=\"text-align: right\">9.5 s<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>App startup (hyperfine, warm cache):<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Command<\/th>\n<th style=\"text-align: right\">HDD<\/th>\n<th style=\"text-align: right\">SSD<\/th>\n<th style=\"text-align: right\">Change<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Firefox<\/td>\n<td style=\"text-align: right\">55.9 ms<\/td>\n<td style=\"text-align: right\">30.7 ms<\/td>\n<td style=\"text-align: right\">1.8x<\/td>\n<\/tr>\n<tr>\n<td>Chromium<\/td>\n<td style=\"text-align: right\">66.9 ms<\/td>\n<td style=\"text-align: right\">38.2 ms<\/td>\n<td style=\"text-align: right\">1.8x<\/td>\n<\/tr>\n<tr>\n<td>Thunar<\/td>\n<td style=\"text-align: right\">85.8 ms<\/td>\n<td style=\"text-align: right\">31.1 ms<\/td>\n<td style=\"text-align: right\">2.8x<\/td>\n<\/tr>\n<tr>\n<td>Ghostty<\/td>\n<td style=\"text-align: right\">78.0 ms<\/td>\n<td style=\"text-align: right\">42.7 ms<\/td>\n<td style=\"text-align: right\">1.8x<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Browser benchmark (Speedometer 3.1):<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Machine<\/th>\n<th style=\"text-align: right\">Score<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>HP Pavilion (Windows, before)<\/td>\n<td style=\"text-align: right\">5.68<\/td>\n<\/tr>\n<tr>\n<td>HP Pavilion (CachyOS, after)<\/td>\n<td style=\"text-align: right\">9.18<\/td>\n<\/tr>\n<tr>\n<td>MacBook Pro M1 (reference)<\/td>\n<td style=\"text-align: right\">19.6<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Battery:<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th style=\"text-align: right\">Old battery<\/th>\n<th style=\"text-align: right\">New battery<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Reported design capacity<\/td>\n<td style=\"text-align: right\">15.5 Wh (misreported)<\/td>\n<td style=\"text-align: right\">43.98 Wh<\/td>\n<\/tr>\n<tr>\n<td>Gauge-learned full capacity<\/td>\n<td style=\"text-align: right\">-<\/td>\n<td style=\"text-align: right\">42.87 Wh<\/td>\n<\/tr>\n<tr>\n<td>Measured delivered energy (100% to 10%)<\/td>\n<td style=\"text-align: right\">-<\/td>\n<td style=\"text-align: right\">37.24 Wh<\/td>\n<\/tr>\n<tr>\n<td>Estimated true capacity<\/td>\n<td style=\"text-align: right\">~15 Wh<\/td>\n<td style=\"text-align: right\">~41.7 Wh<\/td>\n<\/tr>\n<tr>\n<td>Runtime, mixed use<\/td>\n<td style=\"text-align: right\">~2 h<\/td>\n<td style=\"text-align: right\">6+ h<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>Again, I&rsquo;m incredibly pleased with the results here, and I&rsquo;ve since been using this laptop as my daily driver. I have a few other projects I&rsquo;m working on, but at some point I think it would be interesting to dip my toes into the distro development community too. If you&rsquo;re interested at all in OSes, customization, or just free (as in speech + beer) software, I&rsquo;d highly recommend dusting off that old laptop you have around and giving it a try.<\/p>\n<p><img src=\"cachyos-grok-terminal.png\" alt=\"CachyOS terminal with fastfetch next to the Grok agent window\"><\/p>"},{"title":"Navier-Stokes: A Gentle Introduction","link":"https:\/\/ewoodbury.com\/navier-stokes\/","pubDate":"Tue, 15 Sep 2026 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/navier-stokes\/","description":"<p>We know the solution to another Millennium Prize problem! Navier-Stokes is the one problem with the most connection to the physical world; it&rsquo;s probably the problem that anyone has at least a chance to fully understand, at least for the raw equations themselves. I took the opportunity to refamiliarize myself with this math, and to put together an introduction for anyone wanting an intuitive understanding.<\/p>\n<p>This post covers the Navier Stokes equations themselves; I don&rsquo;t cover the contributions of Buckmaster\/Alpoge or OpenAI here yet.<\/p>\n<p>I wrote this to be accessible to anyone with basic physics and algebra knowledge. I still include the full math too, as that&rsquo;s what exposes you the the real equations, but you should be able to skim them and still understand the physical meaning. To fully grasp the math, you&rsquo;ll need familiarity partial differential equations, gradient and divergence, and basic linear algebra (matrix math).<\/p>\n<hr>\n<p>Fluid dynamics is the study of forces on a continuous mass (liquids and gasses), in contrast to rigid body dynamics. In that sense, one could think of fluid dynamics as extending the typical dynamics concepts from Physics 1 (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>F<\/mi><mo>=<\/mo><mi>m<\/mi><mi>a<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">F = ma<\/annotation><\/semantics><\/math><\/span>, <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c4<\/mi><mo>=<\/mo><mi>r<\/mi><mi>F<\/mi><mi>sin<\/mi><mo>\u2061<\/mo><mi>\u03b8<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\tau = rF \\sin\\theta<\/annotation><\/semantics><\/math><\/span>) onto a body of infinitesimally small objects, until you reach the point of modeling the forces on a single continuous, flowing mass. It&rsquo;s similar to going from algebra to calculus, where we go from discrete variables to continuous quantities at tiny scales (This is of course an oversimplification, but it helps frame the question of &ldquo;what is fluid dynamics&rdquo; and explains how we came up with this new set of equations.)<\/p>\n<h2 id=\"mass-balance\">Mass Balance<\/h2>\n<p>The key concepts to know are the physical laws of conservation, and how we can use that to build a balance. We&rsquo;ll start by tracking the volume of an incompressible fluid like water (not truly incompressible but close enough for today). Within an open container, and assuming no chemical reactions or phase changes, water can never be created nor destroyed.<\/p>\n<p>Thus to track water volume, we build a simple balance, which tracks quantity changes against a known conserved constant. In words, our balance is <code>( net inflow ) + ( net outflow ) = 0<\/code>, where zero is the consant saying volume is conserved. In math, that balance is <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo>=<\/mo><mn>0<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">\\nabla \\cdot \\mathbf{u} = 0<\/annotation><\/semantics><\/math><\/span>, where <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold\">u<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbf{u}<\/annotation><\/semantics><\/math><\/span> is what we use for velocity, and <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">\\nabla \\cdot<\/annotation><\/semantics><\/math><\/span> is the divergence operator and essentially means the total outward change in volume, summed across all 3 dimensions.<\/p>\n<p>Volume Balance:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo>=<\/mo><mn>0<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">\n\\nabla \\cdot \\mathbf{u} = 0\n<\/annotation><\/semantics><\/math><\/span><p>In physical terms, this is intuitive: if you hold a straw in a flowing stream, the amount of water in it is always constant, as the volume of water flowing in is exactly equal to the volume flowing out.<\/p>\n<p>Now, if instead of water, let&rsquo;s handle a compressible fluid like air. If you hold a straw in a stream of air, and a large gust blows through it, then for an instant there is more mass of air in the straw. We account for this by adding a density term. The mass balance still holds since no mass is created or destroyed; it just now tracks the change of mass over time inside the volume. Since it&rsquo;s a constant control volume, we divide by the volume to track change in density (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c1<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\rho<\/annotation><\/semantics><\/math><\/span>) instead:<\/p>\n<p>Mass Balance:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mfrac><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>\u03c1<\/mi><\/mrow><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo>+<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mo stretchy=\"false\">(<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mo>=<\/mo><mn>0<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">\n\\frac{\\partial \\rho}{\\partial t} + \\nabla \\cdot (\\rho \\mathbf{u}) = 0\n<\/annotation><\/semantics><\/math><\/span><p>Here, the <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>\u03c1<\/mi><mi mathvariant=\"normal\">\/<\/mi><mi mathvariant=\"normal\">\u2202<\/mi><mi>t<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\partial \\rho \/ \\partial t<\/annotation><\/semantics><\/math><\/span> term is the &ldquo;partial derivative&rdquo; change (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2202<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\partial<\/annotation><\/semantics><\/math><\/span>) in density (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c1<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\rho<\/annotation><\/semantics><\/math><\/span>) over time (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>t<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">t<\/annotation><\/semantics><\/math><\/span>). The divergence (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">\\nabla \\cdot<\/annotation><\/semantics><\/math><\/span>) operator accounts for the possible density difference of flow in versus out. Again, the zero reflects conservation of mass.<\/p>\n<p>Annotated, this is:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><munder><munder><mfrac><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>\u03c1<\/mi><\/mrow><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>change\u00a0inside<\/mtext><\/munder><mo>+<\/mo><munder><munder><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mo stretchy=\"false\">(<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><\/mrow><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>carried\u00a0by\u00a0the\u00a0flow<\/mtext><\/munder><mo>=<\/mo><munder><munder><mn>0<\/mn><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>no\u00a0way\u00a0to\u00a0create\u00a0it<\/mtext><\/munder><\/mrow><annotation encoding=\"application\/x-tex\">\n\\underbrace{\\frac{\\partial \\rho}{\\partial t}}_{\\text{change inside}} +\n\\underbrace{\\nabla \\cdot (\\rho \\mathbf{u})}_{\\text{carried by the flow}} =\n\\underbrace{0}_{\\text{no way to create it}}\n<\/annotation><\/semantics><\/math><\/span><h2 id=\"momentum-balance-euler-equation\">Momentum Balance (Euler Equation)<\/h2>\n<p>Mass balances are easy to visualize and understand, but momentum is another quantity which is conserved. We&rsquo;ll use that property to build a momentum balance, and this is the foundation that will actually build up to Navier-Stokes.<\/p>\n<p>You might remember from basic physics that momentum is <code>mass * velocity<\/code>, and mass is <code>density * volume<\/code>. We&rsquo;ll assume a constant volume throughout this example (a control volume), so we divide by the constant volume to track momentum in terms of density instead. Therefore we&rsquo;ll balance on the momentum term <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c1<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\rho \\mathbf{u}<\/annotation><\/semantics><\/math><\/span>.<\/p>\n<p>For momentum inflows and outflows, we again track the changes at the edge of the container, just like for mass. Here that term is <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"bold\">u<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"bold\">u<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">(\\mathbf{u} \\cdot \\nabla)\\mathbf{u}<\/annotation><\/semantics><\/math><\/span>.<\/p>\n<p>The last term accounts for the change in momentum from forces. Unlike mass, momentum in the volume can also change without anything flowing across the boundary. Any such momentum change must come from an applied force (from Newton&rsquo;s 2nd Law <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>F<\/mi><mo>=<\/mo><mi>m<\/mi><mi>a<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">F = ma<\/annotation><\/semantics><\/math><\/span>).<\/p>\n<p>For fluids, the momentum from external forces is split into two terms. The first is body forces, expressed as <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\rho \\mathbf{b}<\/annotation><\/semantics><\/math><\/span>. <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c1<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\rho<\/annotation><\/semantics><\/math><\/span> is still density, and <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>b<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">b<\/annotation><\/semantics><\/math><\/span> is simply the force per unit mass, which we take as gravity (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mn>9.8<\/mn><mtext>\u2009<\/mtext><msup><mtext>m\/s<\/mtext><mn>2<\/mn><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">9.8\\,\\text{m\/s}^2<\/annotation><\/semantics><\/math><\/span>). This term is exactly the same as for a rigid body (<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><msub><mi>F<\/mi><mtext>gravity<\/mtext><\/msub><mo>=<\/mo><mi>m<\/mi><mi>g<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">F_{\\text{gravity}} = mg<\/annotation><\/semantics><\/math><\/span>) since gravity acts the same on both.<\/p>\n<p>The second term is pressure in the fluid. A fluid is fully made up of particles constantly colliding with each other, with each collision exerting a force. From Newton&rsquo;s 3rd law, every force has an equal and opposite reaction, so all collisions cancel out forces in pairs. The only forces not cancelled are those at the edges, from forces coming from outside of the control volume.<\/p>\n<p>We simply express the net force as <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mo>\u2212<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi>p<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">-\\nabla p<\/annotation><\/semantics><\/math><\/span> (note the minus sign: the force points from high pressure toward low), where <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\nabla<\/annotation><\/semantics><\/math><\/span> (without the dot) is the gradient operator and means the change in all 3 dimensions. Simply having high versus low pressure doesn&rsquo;t make a difference; it&rsquo;s the net force due to the difference in pressures on different sides of the volume.<\/p>\n<p>At this point, we can put together the full momentum balance:<\/p>\n<p>Euler Momentum balance:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mtable width=\"100%\"><mtr><mtd width=\"50%\"><\/mtd><mtd><mrow><munder><munder><mfrac><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mo stretchy=\"false\">(<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><\/mrow><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>change\u00a0inside<\/mtext><\/munder><mo>+<\/mo><munder><munder><mrow><mi>\u03c1<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"bold\">u<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"bold\">u<\/mi><\/mrow><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>carried\u00a0by\u00a0the\u00a0flow<\/mtext><\/munder><mo>=<\/mo><munder><munder><mrow><mo>\u2212<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi>p<\/mi><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>created\u00a0by\u00a0forces<\/mtext><\/munder><\/mrow><\/mtd><mtd width=\"50%\"><\/mtd><mtd><mrow><mtext>(<\/mtext><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mo>\u22c6<\/mo><\/mstyle><mtext>)<\/mtext><\/mrow><\/mtd><\/mtr><\/mtable><annotation encoding=\"application\/x-tex\">\n\\underbrace{\\frac{\\partial (\\rho \\mathbf{u})}{\\partial t}}_{\\text{change inside}} +\n\\underbrace{\\rho (\\mathbf{u} \\cdot \\nabla)\\mathbf{u}}_{\\text{carried by the flow}} =\n\\underbrace{-\\nabla p + \\rho \\mathbf{b}}_{\\text{created by forces}}\n\\tag{$\\star$}\n<\/annotation><\/semantics><\/math><\/span><p>\n(Note this can also be shown with the full outer product term <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mo>\u2297<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">\\otimes<\/annotation><\/semantics><\/math><\/span> or full partial differential equations in 3D.)<\/p>\n<p>In words, the momentum change inside the volume plus the momentum change from inflows and outflows, must always equal the net forces exerted on the volume.<\/p>\n<p>Therefore, we have analogous balances for both mass and momentum now, all descending from the basic laws of physics. These are the basic building blocks from which Navier Stokes is built.<\/p>\n<p>At this point, we already have an incredibly useful governing equation in Euler, both for real applications inf luids (airflow, weather, flow through pipes), and for mathematical explorations. In fact, Euler was the equation attacked by the team of Buckmaster + Alpoge in their recent research.<\/p>\n<p>Next, we&rsquo;ll add viscosity, which is shear (dragging) force of the fluid upon itself, such as when spreading honey.<\/p>\n<h2 id=\"shear-forces-cauchy-momentum\">Shear Forces (Cauchy momentum)<\/h2>\n<p>Now, note that pressure is the common way of thinking about forces in fluids; the outside pressure is always pointed inward. In a more general sense, there can be sideways and diagonal forces on a volume of fluid. We use <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}<\/annotation><\/semantics><\/math><\/span> for this tensor force instead of pressure <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>p<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">p<\/annotation><\/semantics><\/math><\/span>.<\/p>\n<p>In practical terms, <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}<\/annotation><\/semantics><\/math><\/span> is a field in 3-dimensions: in this no-shear case it is entirely determined by the pressure. Water at 1 meter depth is at ~1.1 atm of pressure, so:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mo>\u2248<\/mo><mrow><mo fence=\"true\">[<\/mo><mtable rowspacing=\"0.16em\" columnalign=\"center center center\" columnspacing=\"1em\"><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>1.1<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>5<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mn>0<\/mn><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mn>0<\/mn><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mn>0<\/mn><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>1.1<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>5<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mn>0<\/mn><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mn>0<\/mn><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mn>0<\/mn><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>1.1<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>5<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><\/mtr><\/mtable><mo fence=\"true\">]<\/mo><\/mrow><mtext>\u2009<\/mtext><mtext>Pa<\/mtext><\/mrow><annotation encoding=\"application\/x-tex\">\n\\boldsymbol{\\sigma} \\approx\n\\begin{bmatrix}\n-1.1\\times 10^5 &amp; 0 &amp; 0 \\\\\n0 &amp; -1.1\\times 10^5 &amp; 0 \\\\\n0 &amp; 0 &amp; -1.1\\times 10^5\n\\end{bmatrix} \\, \\text{Pa}\n<\/annotation><\/semantics><\/math><\/span><p>If pressure doubles, those values double as well. But the non-diagonal terms which represent the shear forces are zero here because we&rsquo;re assuming the fluid is at rest; the only force is the inward pressure. When the fluid moves, those terms become nonzero.<\/p>\n<p>Still, in low-viscosity fluids this shear force from movement is small. Imagine putting a layer of water between two flat baking pans and sliding them past each other: there is very little resistance even though the water is moving.<\/p>\n<p>But what if that were a layer of honey instead: now there is significant resistance to sliding the two pans. This is because honey has viscosity: it conducts the shear forces within itself and thus resists a change in momentum even when under the same force (although this shear force is ~100x less than the inward pressure force).<\/p>\n<p>In this case, with honey being sheared between the two sliding pans, our sigma term might instead be:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mo>\u2248<\/mo><mrow><mo fence=\"true\">[<\/mo><mtable rowspacing=\"0.16em\" columnalign=\"center center center\" columnspacing=\"1em\"><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>1.1<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>5<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>0.5<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>3<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>0.5<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>3<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>0.5<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>3<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>1.1<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>5<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>0.5<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>3<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>0.5<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>3<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>0.5<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>3<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mrow><mo>\u2212<\/mo><mn>1.1<\/mn><mo>\u00d7<\/mo><msup><mn>10<\/mn><mn>5<\/mn><\/msup><\/mrow><\/mstyle><\/mtd><\/mtr><\/mtable><mo fence=\"true\">]<\/mo><\/mrow><mtext>\u2009<\/mtext><mtext>Pa<\/mtext><\/mrow><annotation encoding=\"application\/x-tex\">\n\\boldsymbol{\\sigma} \\approx\n\\begin{bmatrix}\n-1.1\\times 10^5 &amp; -0.5\\times 10^3 &amp; -0.5\\times 10^3 \\\\\n-0.5\\times 10^3 &amp; -1.1\\times 10^5 &amp; -0.5\\times 10^3 \\\\\n-0.5\\times 10^3 &amp; -0.5\\times 10^3 &amp; -1.1\\times 10^5\n\\end{bmatrix} \\, \\text{Pa}\n<\/annotation><\/semantics><\/math><\/span><p>With this generalization of shear force included, we arrive at the full Cauchy momentum balance:\nCauchy Momentum Equation:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mtable width=\"100%\"><mtr><mtd width=\"50%\"><\/mtd><mtd><mrow><mi>\u03c1<\/mi><mrow><mo fence=\"true\">[<\/mo><mfrac><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi mathvariant=\"normal\">\u2202<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo>+<\/mo><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"bold\">u<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"bold\">u<\/mi><mo fence=\"true\">]<\/mo><\/mrow><mo>=<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mtd><mtd width=\"50%\"><\/mtd><mtd><mrow><mtext>(<\/mtext><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mo>\u22c6<\/mo><\/mstyle><mtext>)<\/mtext><\/mrow><\/mtd><\/mtr><\/mtable><annotation encoding=\"application\/x-tex\">\n\\rho \\left[ \\frac{\\partial \\mathbf{u}}{\\partial t} + (\\mathbf{u} \\cdot \\nabla)\\mathbf{u} \\right] = \\nabla \\cdot \\boldsymbol{\\sigma} + \\rho \\mathbf{b}\n\\tag{$\\star$}\n<\/annotation><\/semantics><\/math><\/span><p>which, using the material derivative <code>D\/Dt<\/code> from the Euler section, simplifies to the compact Cauchy equation:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo>=<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\n\\rho \\frac{D\\mathbf{u}}{Dt} = \\nabla \\cdot \\boldsymbol{\\sigma} + \\rho \\mathbf{b}\n<\/annotation><\/semantics><\/math><\/span><h2 id=\"final-steps-to-navier-stokes\">Final Steps to Navier-Stokes<\/h2>\n<h3 id=\"viscous-forces\">Viscous Forces<\/h3>\n<p>We already saw that in the no-viscosity case, <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}<\/annotation><\/semantics><\/math><\/span> was simply set as <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mo>\u2212<\/mo><mi>p<\/mi><mi mathvariant=\"bold\">I<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">-p\\mathbf{I}<\/annotation><\/semantics><\/math><\/span>, and with the full Cauchy, <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}<\/annotation><\/semantics><\/math><\/span> does include the additional stress terms.<\/p>\n<p>In math, this can be shown as:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mo>=<\/mo><mo>\u2212<\/mo><mi>p<\/mi><mi mathvariant=\"bold\">I<\/mi><mo>+<\/mo><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">\n\\boldsymbol{\\sigma} = -p\\mathbf{I} + \\boldsymbol{\\sigma}^{v}\n<\/annotation><\/semantics><\/math><\/span><p>\nwhere <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold\">I<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbf{I}<\/annotation><\/semantics><\/math><\/span> is the identity matrix (think of it as multiplying by 1 but in 3D), and <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}^{v}<\/annotation><\/semantics><\/math><\/span> is the term for the viscous forces only.<\/p>\n<p>Next up though, we break down <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}^{v}<\/annotation><\/semantics><\/math><\/span> into smaller terms that inform the forces actually involved. We do so as:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo>=<\/mo><mo stretchy=\"false\">(<\/mo><mtext>some\u00a0tensor\u00a0constant<\/mtext><mo stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mo stretchy=\"false\">(<\/mo><mtext>rate\u00a0of\u00a0deformation<\/mtext><mo stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">\n\\boldsymbol{\\sigma}^{v} = (\\text{some tensor constant}) \\times (\\text{rate of deformation})\n<\/annotation><\/semantics><\/math><\/span><p>\nThe tensor constant is the physical property, it only depends on what the fluid is that we&rsquo;re working with. Water has low viscosity while honey has high, so that tensor constant is higher for honey and it transfers more momentum across the fluid body for a given force.<\/p>\n<p>The rate of deformation is also intuitive: the faster you slide the two pans across the liquid, the greater the force. We can further break down this rate term:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo>=<\/mo><munder><munder><mrow><mn>2<\/mn><mi>\u03bc<\/mi><mi mathvariant=\"bold\">E<\/mi><\/mrow><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>resists\u00a0shearing<\/mtext><\/munder><mo>+<\/mo><munder><munder><mrow><mi>\u03bb<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"bold\">I<\/mi><\/mrow><mo stretchy=\"true\">\u23df<\/mo><\/munder><mtext>resists\u00a0expansion\/compression<\/mtext><\/munder><\/mrow><annotation encoding=\"application\/x-tex\">\n\\boldsymbol{\\sigma}^{v} =\n\\underbrace{2\\mu \\mathbf{E}}_{\\text{resists shearing}} +\n\\underbrace{\\lambda (\\nabla \\cdot \\mathbf{u})\\mathbf{I}}_{\\text{resists expansion\/compression}}\n<\/annotation><\/semantics><\/math><\/span><p>\nThe rate term consists of shear forces (side-to-side) and normal forces (in and out), each of which has its own viscosity constant per fluid: <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03bc<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mu<\/annotation><\/semantics><\/math><\/span> as the shear viscosity, and <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03bb<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\lambda<\/annotation><\/semantics><\/math><\/span> as the bulk viscosity. Again, shear is the sliding force: water versus honey. Bulk viscosity is a bit harder to visualize, but it is the resistance to a rapid change in volume: internal friction during a fast squeeze or expansion. (This is separate from compressiblity, which is the overall ability to change volume regardless of rate.)<\/p>\n<p>Then, <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold\">E<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbf{E}<\/annotation><\/semantics><\/math><\/span> and <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">(\\nabla \\cdot \\mathbf{u})<\/annotation><\/semantics><\/math><\/span> are simply the deformation rates. Faster sliding means higher <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold\">E<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbf{E}<\/annotation><\/semantics><\/math><\/span>, and faster compression\/expansion means higher magnitude of <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">(\\nabla \\cdot \\mathbf{u})<\/annotation><\/semantics><\/math><\/span>. More explicitly, to break down to simplest terms, <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold\">E<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbf{E}<\/annotation><\/semantics><\/math><\/span> can be given as:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"bold\">E<\/mi><mo>=<\/mo><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mfrac><mn>1<\/mn><mn>2<\/mn><\/mfrac><\/mstyle><mrow><mo fence=\"true\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi mathvariant=\"bold\">u<\/mi><mo>+<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><msup><mi mathvariant=\"bold\">u<\/mi><mi>T<\/mi><\/msup><mo fence=\"true\">)<\/mo><\/mrow><\/mrow><annotation encoding=\"application\/x-tex\">\n\\mathbf{E} = \\tfrac{1}{2} \\left( \\nabla \\mathbf{u} + \\nabla \\mathbf{u}^{T} \\right)\n<\/annotation><\/semantics><\/math><\/span><p>\nto be in terms of the same velocity gradient <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\nabla \\mathbf{u}<\/annotation><\/semantics><\/math><\/span> and its transpose <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><msup><mi mathvariant=\"bold\">u<\/mi><mi>T<\/mi><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">\\nabla \\mathbf{u}^{T}<\/annotation><\/semantics><\/math><\/span> (from matrix algebra).<\/p>\n<p>Therefore, we arrive at:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo>=<\/mo><mn>2<\/mn><mi>\u03bc<\/mi><mi mathvariant=\"bold\">E<\/mi><mo>+<\/mo><mi>\u03bb<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"bold\">I<\/mi><mo>=<\/mo><mi>\u03bc<\/mi><mrow><mo fence=\"true\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi mathvariant=\"bold\">u<\/mi><mo>+<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><msup><mi mathvariant=\"bold\">u<\/mi><mi>T<\/mi><\/msup><mo fence=\"true\">)<\/mo><\/mrow><mo>+<\/mo><mi>\u03bb<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"bold\">I<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\n\\boldsymbol{\\sigma}^{v} = 2\\mu \\mathbf{E} + \\lambda(\\nabla \\cdot \\mathbf{u})\\mathbf{I} = \\mu \\left( \\nabla \\mathbf{u} + \\nabla \\mathbf{u}^{T} \\right) + \\lambda(\\nabla \\cdot \\mathbf{u})\\mathbf{I}\n<\/annotation><\/semantics><\/math><\/span><h3 id=\"putting-it-all-together\">Putting it all Together<\/h3>\n<p>At this point, it&rsquo;s a pure algebra exercise, with the goal of simplifying everything down to the basic variables of density <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03c1<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\rho<\/annotation><\/semantics><\/math><\/span>, velocity <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold\">u<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbf{u}<\/annotation><\/semantics><\/math><\/span>, and constants.<\/p>\n<p>First we take our new shear viscosity term <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}^{v}<\/annotation><\/semantics><\/math><\/span> and apply the divergence operator, since that&rsquo;s where Cauchy has the <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi mathvariant=\"bold-italic\">\u03c3<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\boldsymbol{\\sigma}<\/annotation><\/semantics><\/math><\/span> (assuming the viscosities <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03bc<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mu<\/annotation><\/semantics><\/math><\/span> and <span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>\u03bb<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\lambda<\/annotation><\/semantics><\/math><\/span> are uniform across the fuild):<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mrow><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo>=<\/mo><mi>\u03bc<\/mi><msup><mi mathvariant=\"normal\">\u2207<\/mi><mn>2<\/mn><\/msup><mi mathvariant=\"bold\">u<\/mi><mo>+<\/mo><mi>\u03bc<\/mi><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mo>+<\/mo><mi>\u03bb<\/mi><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">\n\\nabla \\cdot \\boldsymbol{\\sigma}^{v} = \\mu \\nabla^2 \\mathbf{u} + \\mu \\nabla(\\nabla \\cdot \\mathbf{u}) + \\lambda \\nabla(\\nabla \\cdot \\mathbf{u})\n<\/annotation><\/semantics><\/math><\/span><span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mtable rowspacing=\"0.25em\" columnalign=\"right left\" columnspacing=\"0em\"><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><mo>=<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mtext>\u2005\u200a<\/mtext><mo>\u27f9<\/mo><mtext>\u2005\u200a<\/mtext><mspace width=\"1em\"\/><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><mo>=<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mrow><mo fence=\"true\">(<\/mo><mo>\u2212<\/mo><mi>p<\/mi><mi mathvariant=\"bold\">I<\/mi><mo>+<\/mo><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo fence=\"true\">)<\/mo><\/mrow><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mtext>\u2005\u200a<\/mtext><mo>\u27f9<\/mo><mtext>\u2005\u200a<\/mtext><mspace width=\"1em\"\/><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><mo>=<\/mo><mo>\u2212<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi>p<\/mi><mo>+<\/mo><mi>\u03bc<\/mi><msup><mi mathvariant=\"normal\">\u2207<\/mi><mn>2<\/mn><\/msup><mi mathvariant=\"bold\">u<\/mi><mo>+<\/mo><mo stretchy=\"false\">(<\/mo><mi>\u03bb<\/mi><mo>+<\/mo><mi>\u03bc<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mstyle><\/mtd><\/mtr><\/mtable><annotation encoding=\"application\/x-tex\">\n\\begin{aligned}\n\\rho \\frac{D\\mathbf{u}}{Dt} &amp;= \\nabla \\cdot \\boldsymbol{\\sigma} + \\rho \\mathbf{b} \\\\\n\\implies \\quad \\rho \\frac{D\\mathbf{u}}{Dt} &amp;= \\nabla \\cdot \\left(-p\\mathbf{I} + \\boldsymbol{\\sigma}^{v}\\right) + \\rho \\mathbf{b} \\\\\n\\implies \\quad \\rho \\frac{D\\mathbf{u}}{Dt} &amp;= -\\nabla p + \\mu \\nabla^2 \\mathbf{u} + (\\lambda + \\mu) \\nabla(\\nabla \\cdot \\mathbf{u}) + \\rho \\mathbf{b}\n\\end{aligned}\n<\/annotation><\/semantics><\/math><\/span><p>And that&rsquo;s it! We&rsquo;ve now arrived at the full Navier-Stokes equation governing fluid flow, all in terms of basic, measurable fluid properties.<\/p>\n<p>Full Navier-Stokes (compressible fluid):<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mtable width=\"100%\"><mtr><mtd width=\"50%\"><\/mtd><mtd><mrow><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo>=<\/mo><mo>\u2212<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi>p<\/mi><mo>+<\/mo><mi>\u03bc<\/mi><msup><mi mathvariant=\"normal\">\u2207<\/mi><mn>2<\/mn><\/msup><mi mathvariant=\"bold\">u<\/mi><mo>+<\/mo><mo stretchy=\"false\">(<\/mo><mi>\u03bb<\/mi><mo>+<\/mo><mi>\u03bc<\/mi><mo stretchy=\"false\">)<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo stretchy=\"false\">(<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo stretchy=\"false\">)<\/mo><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mtd><mtd width=\"50%\"><\/mtd><mtd><mrow><mtext>(<\/mtext><mstyle scriptlevel=\"0\" displaystyle=\"false\"><mo>\u22c6<\/mo><\/mstyle><mtext>)<\/mtext><\/mrow><\/mtd><\/mtr><\/mtable><annotation encoding=\"application\/x-tex\">\n\\rho \\frac{D\\mathbf{u}}{Dt} = -\\nabla p + \\mu \\nabla^2 \\mathbf{u} + (\\lambda + \\mu) \\nabla(\\nabla \\cdot \\mathbf{u}) + \\rho \\mathbf{b}\n\\tag{$\\star$}\n<\/annotation><\/semantics><\/math><\/span><p>We&rsquo;ve touched on the simplifications across this derivation, but to show them together here as they&rsquo;ll often be applied to show as a simplified form:<\/p>\n<span class=\"katex\"><math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\" display=\"block\"><semantics><mtable rowspacing=\"0.25em\" columnalign=\"right left\" columnspacing=\"0em\"><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mtext>Euler:<\/mtext><mspace width=\"1em\"\/><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo>=<\/mo><mn>0<\/mn><mtext>\u2005\u200a<\/mtext><mtext>\u2005\u200a<\/mtext><mo stretchy=\"false\">(<\/mo><mtext>no\u00a0viscous\u00a0terms<\/mtext><mo stretchy=\"false\">)<\/mo><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo>=<\/mo><mo>\u2212<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi>p<\/mi><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mtext>Incompressible\u00a0NS:<\/mtext><mspace width=\"1em\"\/><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><msup><mi mathvariant=\"bold-italic\">\u03c3<\/mi><mi>v<\/mi><\/msup><mo>=<\/mo><mn>2<\/mn><mi>\u03bc<\/mi><mi mathvariant=\"bold\">E<\/mi><mtext>\u2005\u200a<\/mtext><mtext>\u2005\u200a<\/mtext><mo stretchy=\"false\">(<\/mo><mi>\u03bb<\/mi><mtext>-term\u00a0dead:\u00a0<\/mtext><mi mathvariant=\"normal\">\u2207<\/mi><mo>\u22c5<\/mo><mi mathvariant=\"bold\">u<\/mi><mo>=<\/mo><mn>0<\/mn><mo stretchy=\"false\">)<\/mo><\/mrow><\/mstyle><\/mtd><\/mtr><mtr><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><\/mrow><\/mstyle><\/mtd><mtd><mstyle scriptlevel=\"0\" displaystyle=\"true\"><mrow><mrow><\/mrow><mi>\u03c1<\/mi><mfrac><mrow><mi>D<\/mi><mi mathvariant=\"bold\">u<\/mi><\/mrow><mrow><mi>D<\/mi><mi>t<\/mi><\/mrow><\/mfrac><mo>=<\/mo><mo>\u2212<\/mo><mi mathvariant=\"normal\">\u2207<\/mi><mi>p<\/mi><mo>+<\/mo><mi>\u03bc<\/mi><msup><mi mathvariant=\"normal\">\u2207<\/mi><mn>2<\/mn><\/msup><mi mathvariant=\"bold\">u<\/mi><mo>+<\/mo><mi>\u03c1<\/mi><mi mathvariant=\"bold\">b<\/mi><\/mrow><\/mstyle><\/mtd><\/mtr><\/mtable><annotation encoding=\"application\/x-tex\">\n\\begin{aligned}\n\\text{Euler:} \\quad &amp; \\boldsymbol{\\sigma}^{v} = 0 \\;\\; (\\text{no viscous terms}) \\\\\n&amp; \\rho \\frac{D\\mathbf{u}}{Dt} = -\\nabla p + \\rho \\mathbf{b} \\\\[1.5em]\n\\text{Incompressible NS:} \\quad &amp; \\boldsymbol{\\sigma}^{v} = 2\\mu \\mathbf{E} \\;\\; (\\lambda \\text{-term dead: } \\nabla \\cdot \\mathbf{u} = 0) \\\\\n&amp; \\rho \\frac{D\\mathbf{u}}{Dt} = -\\nabla p + \\mu \\nabla^2 \\mathbf{u} + \\rho \\mathbf{b}\n\\end{aligned}\n<\/annotation><\/semantics><\/math><\/span><p>The whole derivation, end to end, all built from the basic momentum balance:<\/p>\n<div class=\"mermaid\">flowchart TD\nA[\"Momentum balance<br\/>\u2202(\u03c1u)\/\u2202t + \u03c1(u\u00b7\u2207)u = forces\"]\nB[\"Mass balance<br\/>\u2202\u03c1\/\u2202t + \u2207\u00b7(\u03c1u) = 0\"]\nC[\"Cauchy momentum<br\/>\u03c1 Du\/Dt = \u2207\u00b7\u03c3 + \u03c1b<br\/><i>\u03c3: push force<\/i>\"]\nD[\"Euler equation<br\/>\u03c1 Du\/Dt = \u2212\u2207p + \u03c1b\"]\nE[\"Navier-Stokes<br\/>\u03c1 Du\/Dt = \u2212\u2207p + \u03bc\u2207\u00b2u + (\u03bb+\u03bc)\u2207(\u2207\u00b7u) + \u03c1b\"]\nA -->|\"Contact forces \u03c3\"| C\nB --> C\nC -->|\"\u03c3 = \u2212pI<br\/>(push only, no shear)\"| D\nC -->|\"\u03c3 = \u2212pI + 2\u03bcE + \u03bb(\u2207\u00b7u)I<br\/>(push + drag)\"| E\n<\/div>\n<hr>\n<h2 id=\"the-2026-proposed-solution\">The 2026 Proposed Solution<\/h2>\n<p>Although I won&rsquo;t deeply analyze the Millennium Prize problem and solution itself, I do think it&rsquo;s worth a passing discussion after we walked through the full derivation.<\/p>\n<p>The problem essentially asks the question: does Navier-Stokes hold up across all time-scales and length-scales, or does it break down under some sort of special conditions? In other words, do these equations result in an impossible infinite fluid velocity under some physical conditions?<\/p>\n<p>There were two dimensions of the problem under active research: without vs. with viscous forces (Euler equation vs. full N-S), with Buckmaster\/Alpoge tackling Euler and OAI tackling full N-S. The second dimension is forced versus unforced, where forced means some external force is applied to the system to cause a perturbation in the fluid and cause the breakdown, and unforced goes without any external perturbation. I&rsquo;m not familiar with the recent research, but both Buckmaster\/Alpoge and OAI tackled the forced setup while unforced was the much more common current research area, and this fact was apparently the major driver of the research privacy controversy.<\/p>\n<p>These solutions are a massive milestone for fluid dynamics and mathematics overall. I hope that this can be a spark for further mathematics research into related and new problems, and that it can be an opportunity for the wider world to get some insight and I dare say even enjoyment into this typically inaccessible field.<\/p>\n<h2 id=\"wrap-up\">Wrap-up<\/h2>\n<p>In my opinion this is an elegant derivation, in that it does not depend on any esoteric concepts, mathematical constructions, or 4+ dimensional systems. Every single term has a true physical analogue that anyone could visualize. Yet despite the simplicity, it builds a governing equation that can be the subject of study of both engineering and pure math for decades. It also feels stunning to me that only in 2026, have we found the true limits of where this equation can be applied.<\/p>\n<p>My background is Chemical Engineeering, so it was fun to see something I spent a meaningful fraction of my life studying, suddenly becoming front page news and fuel such controversy on X and elsewhere. At this point, it&rsquo;s inevitable that frontier math discovery will be dominated by AI. I think it&rsquo;s a minor tragedy that in all likelihood, only one Millenium Prize problem will have ever been solved end-to-end by a human mind. Yet, as I hope you saw here, math can truly be a beautiful form to describe the natural world, and I hope this is a small push toward sharing that beauty.<\/p>"},{"title":"Autoformalizer: Build math proofs in Lean with LLMs","link":"https:\/\/ewoodbury.com\/posts\/2025-09-27_lean_autoformalizer\/","pubDate":"Sun, 28 Sep 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-09-27_lean_autoformalizer\/","description":"<p>I built a project called Autoformalizer, which is a simple Python tool that builds verifiable proofs for simple math theorems in Lean 4.<\/p>\n<p>Autoformalizer is a system that passes natural language proof statements to an LLM to iteratively build a formal proof, then passes the proof to the Lean compiler to verify its correctness. It also automatically retries failed proof attempts with intelligent prompting using the error messages from Lean.<\/p>\n<p>This was an incredibly fun and satisfying project! I&rsquo;ve been interested in pure math recently after half a decade being away from the math of my undergraduate engineering degree. Plus, it was my first foray into building eval systems for LLMs.<\/p>\n<p>You can check out the full project on <a href=\"https:\/\/github.com\/ewoodbury\/lean-autoformalizer\" target=\"_blank\" rel=\"noopener\">GitHub<\/a>.<\/p>\n<h2 id=\"results\">Results<\/h2>\n<p>As of today, Autformalizer successfully proves 24 out of 26 simple theorems from an unseen test set, using Grok-4-fast.<\/p>\n<p>On the first attempt, it only proves 11\/26, but this increases to 24\/26 using up to 4 retries, which shows the intelligent re-prompting upon errors is effective.<\/p>\n<pre tabindex=\"0\"><code>=== Evaluation Summary ===\nDataset: datasets\/test.jsonl\nItems evaluated: 26\nSuccesses: 24 (92.3%)\nCompile-rate@1: 42.3%\nPass@K:\nPass@1: 42.3%\nPass@5: 92.3%\n<\/code><\/pre><details>\n<summary>Click for full results<\/summary>\n<pre tabindex=\"0\"><code>=== Evaluation Summary ===\nDataset: datasets\/test.jsonl\nItems evaluated: 26\nSuccesses: 24 (92.3%)\nCompile-rate@1: 42.3%\nPass@K:\nPass@1: 42.3%\nPass@5: 92.3%\nAttempts per proof:\nmean=2.23, median=2.0, p90=4.5\nTime per proof (s):\nmean=52.97, median=22.31, p90=162.67\nPer-item outcomes:\n\u2705 nat_succ_mul_expand :: attempts=1, success_rank=1, time=10.60s, pass[@1:Y @5:Y]\n\u2705 eq_symm :: attempts=1, success_rank=1, time=4.26s, pass[@1:Y @5:Y]\n\u2705 prop_and_left :: attempts=1, success_rank=1, time=4.05s, pass[@1:Y @5:Y]\n\u2705 prop_and_right :: attempts=1, success_rank=1, time=3.96s, pass[@1:Y @5:Y]\n\u2705 nat_add_right_cancel :: attempts=4, success_rank=8, time=80.62s, pass[@1:N @5:Y]\n\u2705 nat_succ_lt_succ :: attempts=1, success_rank=1, time=6.69s, pass[@1:Y @5:Y]\n\u2705 nat_zero_add_left :: attempts=1, success_rank=1, time=5.00s, pass[@1:Y @5:Y]\n\u2705 list_reverse_reverse :: attempts=1, success_rank=1, time=6.09s, pass[@1:Y @5:Y]\n\u2705 list_length_reverse :: attempts=4, success_rank=9, time=70.87s, pass[@1:N @5:Y]\n\u2705 list_map_append :: attempts=3, success_rank=5, time=43.57s, pass[@1:N @5:Y]\n\u2705 set_inter_assoc :: attempts=1, success_rank=1, time=10.86s, pass[@1:Y @5:Y]\n\u2705 set_union_self :: attempts=2, success_rank=3, time=21.18s, pass[@1:N @5:Y]\n\u2705 set_inter_self :: attempts=2, success_rank=2, time=20.38s, pass[@1:N @5:Y]\n\u2705 int_mul_assoc :: attempts=1, success_rank=1, time=4.51s, pass[@1:Y @5:Y]\n\u2705 int_distrib_left :: attempts=2, success_rank=3, time=78.94s, pass[@1:N @5:Y]\n\u2705 int_neg_add :: attempts=5, success_rank=15, time=275.90s, pass[@1:N @5:Y]\n\u2705 function_injective_comp :: attempts=4, success_rank=11, time=139.31s, pass[@1:N @5:Y]\n\u274c function_surjective_comp :: attempts=5, success_rank=-, time=186.02s, pass[@1:N @5:N]\n\u2705 eq_congr_fun :: attempts=2, success_rank=2, time=23.83s, pass[@1:N @5:Y]\n\u2705 eq_congr_arg :: attempts=2, success_rank=2, time=23.44s, pass[@1:N @5:Y]\n\u2705 nat_succ_inj :: attempts=2, success_rank=4, time=35.26s, pass[@1:N @5:Y]\n\u2705 nat_le_succ_self :: attempts=2, success_rank=3, time=30.39s, pass[@1:N @5:Y]\n\u2705 nat_lt_succ_self :: attempts=1, success_rank=1, time=10.18s, pass[@1:Y @5:Y]\n\u2705 prop_or_true :: attempts=1, success_rank=1, time=8.57s, pass[@1:Y @5:Y]\n\u2705 nat_dvd_refl :: attempts=3, success_rank=6, time=85.59s, pass[@1:N @5:Y]\n\u274c nat_dvd_trans :: attempts=5, success_rank=-, time=187.06s, pass[@1:N @5:N]\n\u2713 Tests and evaluation metrics completed\n<\/code><\/pre><\/details>\n<br \/>\n<p>Autoformalizer also includes a CLI that lets you run any theorem you want, to see if the LLM can prove it:<\/p>\n<pre tabindex=\"0\"><code>$ uv run autoformalizer decode\nStarting interactive decoder...\nStatement: If functions f and g are surjective, then f\u2218g is surjective.\nProof steps (optional, separate entries with &#39;;&#39;):\n=== Lean Candidate ===\nimport Mathlib.Function\nvariable {\u03b1 \u03b2 \u03b3 : Type*}\ntheorem surjective_comp {f : \u03b2 \u2192 \u03b3} {g : \u03b1 \u2192 \u03b2} (hf : Surjective f) (hg : Surjective g) :\nSurjective (f \u2218 g) := by\nintro z\nrcases hf z with \u27e8y, hy\u27e9\nrcases hg y with \u27e8x, hx\u27e9\nuse x\nrw [hx, hy]\nValidation: Success! (7.21s)\n<\/code><\/pre><h2 id=\"lean\">Lean<\/h2>\n<p>Lean is a functional programming language, but more specifically an interactive theorem prover, which means it can be used to construct and verify mathematical proofs. A significant amount of math research over the last few years has heavily utilized Lean. One notable example is Kevin Buzzard and Richard Taylor&rsquo;s work to formalize <a href=\"https:\/\/imperialcollegelondon.github.io\/FLT\/blueprint.pdf\" target=\"_blank\" rel=\"noopener\">Fermat&rsquo;s Last Theorem in Lean<\/a>.<\/p>\n<p>Lean in particular has been picked up by AI research teams alongside the rise of LLMs. <a href=\"https:\/\/deepmind.google\/discover\/blog\/ai-solves-imo-problems-at-silver-medal-level\/\" target=\"_blank\" rel=\"noopener\">DeepMind&rsquo;s AlphaProof<\/a> used Lean 4 and won the 2024 IMO Silver Medal (<a href=\"https:\/\/storage.googleapis.com\/deepmind-media\/DeepMind.com\/Blog\/imo-2024-solutions\/index.html\" target=\"_blank\" rel=\"noopener\">code<\/a>). The startup <a href=\"https:\/\/www.math.inc\/\" target=\"_blank\" rel=\"noopener\">Math Inc.<\/a> recently published a complete formalization of the strong Prime Number Theorem in Lean 4, and is working to expand their autoformalizer.<\/p>\n<p>It&rsquo;s an exciting time for Lean, and for pure math in general!<\/p>\n<h2 id=\"architecture\">Architecture<\/h2>\n<p>The logical flow for the Autoformalizer is as follows:<\/p>\n<ol>\n<li>A proof statement is passed into Autoformalizer (via CLI from user, or from test dataset)<\/li>\n<li>The statement is passed to the LLM to generate a candidate Lean proof<\/li>\n<li>The candidate proof passed to the Lean compiler to verify its correctness\ni. If it compiles, return success\nii. If it doesn&rsquo;t compile, construct an updated prompt using the error message and pass back to LLM\niii. If max attempts is reached, return failure<\/li>\n<li>Upon failures, or if configured for initial runs, also pass the candidate proof and statement to the Beam Search Executor, which generates and tests multiple candidates in parallel with various temperature and beam width settings.<\/li>\n<\/ol>\n<div class=\"mermaid\">flowchart TD\nuser[\"CLI \/ Scripts \/ Tests\"] --> decoder[\"decode.generate_lean_proof\"]\ndecoder -->|build prompt| llmDecode[\"ModelClient (initial call to LLM)\"]\nllmDecode -->|Lean candidate string| decoder\ndecoder -->|CandidateLean| validator[\"decode.validate_lean_code\"]\nvalidator -->|valid candidate| executorLoop[\"executor.AutoformalizationExecutor\"]\nexecutorLoop -->|schedule attempt \/ retry| beamExec[\"executor.BeamSearchExecutor\"]\nbeamExec -->|candidate batch| executorLoop\nbeamExec -->|LLM prompt| llmBeam[\"ModelClient (retry generations)\"]\nllmBeam -->|Lean candidate| cache[\"executor.ExecutorCache\"]\ncache --> beamExec\nexecutorLoop -->|compile candidate| leanEnv[\"lake env lean\"]\nleanEnv --> decision{Compile OK?}\ndecision -->|yes| success[\"Success\"]\ndecision -->|no| errorClassifier[\"executor.ErrorClassifier\"]\nerrorClassifier -->|repair prompt| beamExec\nerrorClassifier -->|signal retry| executorLoop\nexecutorLoop -->|max attempts exhausted| failure[\"Failure\"]\nsuccess --> outputs[\"AutoformalizationResult to CLI\/tests\"]\nfailure --> outputs\n<\/div>\n<h2 id=\"initial-prompt\">Initial Prompt<\/h2>\n<p>The initial prompt is extremely simple: it explains the task to the LLM, and the provides a simple example of a statement plus proof pair.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-python\" data-lang=\"python\"><span style=\"display:flex;\"><span>ENGLISH_TO_LEAN_PROMPT <span style=\"color:#f92672\">=<\/span> (\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Given this mathematical statement in English, generate a complete Lean 4 theorem:<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;English: <\/span><span style=\"color:#e6db74\">{statement}<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Steps: <\/span><span style=\"color:#e6db74\">{steps}<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Generate the complete Lean theorem including imports and proof.<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Use tactic mode with &#39;by&#39; and keep it concise.<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Example:<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#39;English: &#34;For all natural numbers a and b, a + b = b + a&#34;<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#39;Steps: [&#34;Use commutativity of addition on naturals&#34;]<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Lean:<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;```lean<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;import Mathlib\/Data\/Nat\/Basic<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;theorem add_comm_nat (a b : Nat) : a + b = b + a := by<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34; rw [Nat.add_comm]<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;```<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Your turn:<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;English: <\/span><span style=\"color:#e6db74\">{statement}<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Steps: <\/span><span style=\"color:#e6db74\">{steps}<\/span><span style=\"color:#ae81ff\">\\n\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Lean:<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>)\n<\/span><\/span><\/code><\/pre><\/div><p>I haven&rsquo;t done any tuning on this prompt yet. My intuition is providing additional examples would be very helpful, especially if we try with more complex proofs. I think it could also clearly outline what imports and Mathlib modules are available, as import errors are a common failure mode currently.<\/p>\n<h2 id=\"error-recovery\">Error Recovery<\/h2>\n<p>One key feature of this Autoformalizer is its ability to automatically retry failed proof attempts to prompt retries. This is very effective: the proof success rate increases from 11\/26 (42%) on the first attempt to 24\/26 (92%) with up to 4 retries!<\/p>\n<h3 id=\"re-prompts\">Re-prompts<\/h3>\n<p>The reprompt strategy is straightforward:<\/p>\n<ol>\n<li>When a proof candidate fails to compile, save the error message from the Lean compiler<\/li>\n<li>Classify the error message into one of <code>type_mismatch<\/code>, <code>tactic_failed<\/code>, <code>missing_premise<\/code>, <code>syntax_error<\/code>, or <code>other<\/code><\/li>\n<li>Construct a new prompt that includes the original proof statement, the failed candidate, and the classified error message<\/li>\n<li>Pass the new prompt back to the LLM to generate a new candidate proof<\/li>\n<\/ol>\n<p>The error classification uses simple regex logic.<\/p>\n<details>\n<summary>Click to expand error classification logic<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-python\" data-lang=\"python\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Error pattern matchers with categories<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>ERROR_PATTERNS <span style=\"color:#f92672\">=<\/span> [\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Unknown identifier patterns<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>UNKNOWN_IDENTIFIER, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;unknown identifier &#39;([^&#39;]+)&#39;&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>UNKNOWN_IDENTIFIER, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;failed to resolve &#39;([^&#39;]+)&#39;&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>UNKNOWN_IDENTIFIER, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;&#39;([^&#39;]+)&#39; has not been declared&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Type mismatch patterns<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TYPE_MISMATCH, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;type mismatch&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TYPE_MISMATCH, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;expected .+, got .+&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TYPE_MISMATCH, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;has type .+ but is expected to have type&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Tactic failure patterns<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TACTIC_FAILED, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;tactic failed&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TACTIC_FAILED, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;unsolved goals&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TACTIC_FAILED, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;goal state:&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>TACTIC_FAILED, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;invalid tactic&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Missing premise patterns<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>MISSING_PREMISE, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;could not synthesize&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>MISSING_PREMISE, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;no applicable rules&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>MISSING_PREMISE, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;failed to prove&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Syntax error patterns<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>SYNTAX_ERROR, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;unexpected token&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>SYNTAX_ERROR, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;expected &#39;\\)&#39;&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>SYNTAX_ERROR, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;expected &#39;\\]&#39;&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>SYNTAX_ERROR, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;expected &#39;\\}&#39;&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span> (ErrorCategory<span style=\"color:#f92672\">.<\/span>SYNTAX_ERROR, re<span style=\"color:#f92672\">.<\/span>compile(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;invalid expression&#34;<\/span>)),\n<\/span><\/span><span style=\"display:flex;\"><span>]\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> <span style=\"color:#a6e22e\">classify_lean_error<\/span>(stderr: str) <span style=\"color:#f92672\">-&gt;<\/span> list[LeanError]:\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;&#34;&#34;Parse stderr and return classified errors with fix suggestions.&#34;&#34;&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">not<\/span> stderr<span style=\"color:#f92672\">.<\/span>strip():\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> []\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> errors <span style=\"color:#f92672\">=<\/span> []\n<\/span><\/span><span style=\"display:flex;\"><span> lines <span style=\"color:#f92672\">=<\/span> stderr<span style=\"color:#f92672\">.<\/span>strip()<span style=\"color:#f92672\">.<\/span>split(<span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#ae81ff\">\\n<\/span><span style=\"color:#e6db74\">&#34;<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> line <span style=\"color:#f92672\">in<\/span> lines:\n<\/span><\/span><span style=\"display:flex;\"><span> line <span style=\"color:#f92672\">=<\/span> line<span style=\"color:#f92672\">.<\/span>strip()\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">not<\/span> line:\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">continue<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Try to classify each line as a potential error<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> error <span style=\"color:#f92672\">=<\/span> _process_error_block([line], stderr)\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> error:\n<\/span><\/span><span style=\"display:flex;\"><span> errors<span style=\"color:#f92672\">.<\/span>append(error)\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># If no errors found, create a generic one<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> errors <span style=\"color:#66d9ef\">if<\/span> errors <span style=\"color:#66d9ef\">else<\/span> [_create_other_error(stderr)]\n<\/span><\/span><\/code><\/pre><\/div><\/details>\n<br \/>\n<p>Then, the new prompt is constructed with a simple guide according to the error category. For example, for an <code>unknown_identifier<\/code> error, the prompt finds what was unknown then suggests the LLM to import it properly:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-python\" data-lang=\"python\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">if<\/span> category <span style=\"color:#f92672\">==<\/span> ErrorCategory<span style=\"color:#f92672\">.<\/span>UNKNOWN_IDENTIFIER:\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\"># Extract identifier name if possible<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">=<\/span> re<span style=\"color:#f92672\">.<\/span>search(<span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">&#34;&#39;([^&#39;]+)&#39;&#34;<\/span>, message)\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">match<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span> identifier <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">match<\/span><span style=\"color:#f92672\">.<\/span>group(<span style=\"color:#ae81ff\">1<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span> suggestions<span style=\"color:#f92672\">.<\/span>extend(\n<\/span><\/span><span style=\"display:flex;\"><span> [\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">f<\/span><span style=\"color:#e6db74\">&#34;Add import for &#39;<\/span><span style=\"color:#e6db74\">{<\/span>identifier<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#39;&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">f<\/span><span style=\"color:#e6db74\">&#34;Check spelling of &#39;<\/span><span style=\"color:#e6db74\">{<\/span>identifier<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#39;&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">f<\/span><span style=\"color:#e6db74\">&#34;Use correct namespace for &#39;<\/span><span style=\"color:#e6db74\">{<\/span>identifier<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#39;&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> ]\n<\/span><\/span><span style=\"display:flex;\"><span> )\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">else<\/span>:\n<\/span><\/span><span style=\"display:flex;\"><span> suggestions<span style=\"color:#f92672\">.<\/span>append(<span style=\"color:#e6db74\">&#34;Check imports and identifier spelling&#34;<\/span>)\n<\/span><\/span><\/code><\/pre><\/div><p>I spent some time tuning the prompt templates, but for the most part the basic context was all that was needed. This type of rapid, targeted feedback loop seems to be one of the most effective ways to getting correct results from LLMs (similar to type checks\/compiler errors for LLMs generating code).<\/p>\n<h3 id=\"beam-search\">Beam search<\/h3>\n<p>Beam search is a parallel sampling strategy that improves error recovery for proofs. Instead of generating only one proof candidate during retries, it generates several candidates with slightly different randomness.<\/p>\n<p>On each subsequent retry, the executor increases the beam width and temperature to explore a wider candidate space, increasing the odds of finding a valid proof.<\/p>\n<ul>\n<li>\n<p><code>BeamSearchExecutor<\/code> constructs prompts for each beam slot. Initial attempts call into the decode prompt template; repair attempts call <code>generate_repair_prompt<\/code> with the primary classified error.<\/p>\n<\/li>\n<li>\n<p>The default <code>beam_schedule<\/code> is <code>[1, 3, 3, 5, 5]<\/code>, meaning the first attempt generates 1 candidate, and later retries generate up to 5 candidates.<\/p>\n<\/li>\n<li>\n<p>The default <code>temperature_schedule<\/code> is <code>[0.3, 0.5, 0.5, 0.7, 0.8]<\/code>, which effectively means the LLM starts out more deterministic and gradually becomes more exploratory.<\/p>\n<\/li>\n<\/ul>\n<p>Again, I only spent limited time tuning this for now; increasing the beam width and temperature helped somewhat, but not nearly as much as re-prompting with context. The additional calls do significantly increase the runtime and cost, so this is what I would focus on optimizing before running this at scale, and on harder theorems.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<h3 id=\"future-work\">Future work<\/h3>\n<p>Again, this was a fun project! I was very pleased with being able to build an end-to-end autoformalization system around Lean in about a week, especially given it can successfully prove the vast majority of the simple theorems in the test set.<\/p>\n<p>If I continue this project, the biggest next step is testing against more complex theorems, and to run tests without providing suggested steps to the LLM. The current train and test sets are comprised of simple or even trivial theorems, with half of them being only a single-line proof. This is enough to still evaluate the LLM&rsquo;s adherence to correct Lean 4 syntax, but it doesn&rsquo;t reach the level of testing mathematical reasoning.<\/p>\n<p>It might also be fun to expand this to be a true LLM evaluation suite, where any LLM can be plugged in and have its mathemtical reasonning tested. I found some published benchmarks on this such as Epoch AI&rsquo;s <a href=\"https:\/\/epoch.ai\/frontiermath\" target=\"_blank\" rel=\"noopener\">FrontierMath<\/a>, but it&rsquo;s still a relatively unexplored area.<\/p>\n<h3 id=\"formal-methods-and-llm-reliability\">Formal methods and LLM reliability<\/h3>\n<p>Hallucination and unpredictability are well-known issues with current LLMs. Formal methods seem like a promising avenue for improving the reliability of LLMs in production or safety-critical settings. The same way that a compiler enforces the correctness of a program&rsquo;s syntax and types, formal methods can enforce the correctness of an entire program.<\/p>\n<p>Pure math is a perfect playground for exploring this world with LLMs. There&rsquo;s a corpus of formalized proofs in Lean, and the correctness of a proof can be verified by the Lean compiler. Yet there&rsquo;s also a huge number of both solved and unsolved problems that require creativity.<\/p>\n<p>I think math research will be impacted heavily by LLMs in the next few years, the same way that programming has already been irreversibly changed. Still, I think the more interesting aspect is whether Lean, or formal methods in general, can be used to improve the reliability of AI systems at their core.<\/p>"},{"title":"A simple web scraper in Rust","link":"https:\/\/ewoodbury.com\/posts\/2025-09-10_rust_web_scraper\/","pubDate":"Wed, 10 Sep 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-09-10_rust_web_scraper\/","description":"<p>I&rsquo;ve been itching to learn Rust for a while! Here I document my brief adventure building a simple Rust web scraper over the course of an afternoon.<\/p>\n<p>My main goal here is of course to learn the very basics of Rust. My secondary goal is to build a method to save a large number of technical posts for for an LLM fine-tuning project I&rsquo;ve only just started.<\/p>\n<p>Lastly, disclaimer that I heavily used an AI Agent to write the initial code here. I used VS Code&rsquo;s GH Copilot agent for this project with Claude 4 as the model. I&rsquo;ve also had lots of success with Cursor, Claude Code, and Kilo Code (to a smaller extent). I&rsquo;ve had a lot of success using agents for general setup and learning for projects that are just for fun like this. I definitely have a long ways to go in learning Rust fundamentals!<\/p>\n<p>If you want to check out the full code, take a look on <a href=\"https:\/\/github.com\/ewoodbury\/rust-web-scraper\" target=\"_blank\" rel=\"noopener\">Github<\/a>.<\/p>\n<h2 id=\"getting-started\">Getting Started<\/h2>\n<h3 id=\"basic-setup\">Basic Setup<\/h3>\n<p>I installed and set up my Rust environment using <a href=\"https:\/\/rustup.rs\/\" target=\"_blank\" rel=\"noopener\">rustup<\/a>. I then created a new binary project using <code>cargo new rust-web-scraper<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>$ cargo new rust-web-scraper\n<\/span><\/span><span style=\"display:flex;\"><span> Created binary <span style=\"color:#f92672\">(<\/span>application<span style=\"color:#f92672\">)<\/span> <span style=\"color:#e6db74\">`<\/span>rust-web-scraper<span style=\"color:#e6db74\">`<\/span> package\n<\/span><\/span><span style=\"display:flex;\"><span>$ cd rust-web-scraper\n<\/span><\/span><\/code><\/pre><\/div><p>Up next, I set up my cargo.toml with my main project dependencies. From my research, <code>reqwest<\/code> was the most popular HTTP client library, and <code>scraper<\/code> was a popular HTML parsing library. I also added <code>tokio<\/code> for async runtime support. This should be a very conventional stack for Rust.<\/p>\n<p>As a side note, Rust dependencies are just like Poetry for Python! It uses the exact same .toml -&gt; .lock file setup, which I generally like from Poetry. I did some Googling and found that Poetry was heavily inspired by Cargo, which makes sense.<\/p>\n<details>\n<summary>Expand to view full cargo.toml:<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-toml\" data-lang=\"toml\"><span style=\"display:flex;\"><span>[<span style=\"color:#a6e22e\">package<\/span>]\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">name<\/span> = <span style=\"color:#e6db74\">&#34;web-scraper&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">version<\/span> = <span style=\"color:#e6db74\">&#34;0.1.0&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">edition<\/span> = <span style=\"color:#e6db74\">&#34;2025&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">authors<\/span> = [<span style=\"color:#e6db74\">&#34;Ethan Woodbury &lt;ethwoodbury@gmail.com&gt;&#34;<\/span>]\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">description<\/span> = <span style=\"color:#e6db74\">&#34;A web scraper for technical blog posts&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">license<\/span> = <span style=\"color:#e6db74\">&#34;MIT&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>[<span style=\"color:#a6e22e\">dependencies<\/span>]\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># HTTP client<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">reqwest<\/span> = { <span style=\"color:#a6e22e\">version<\/span> = <span style=\"color:#e6db74\">&#34;0.11&#34;<\/span>, <span style=\"color:#a6e22e\">features<\/span> = [<span style=\"color:#e6db74\">&#34;json&#34;<\/span>, <span style=\"color:#e6db74\">&#34;gzip&#34;<\/span>] }\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">tokio<\/span> = { <span style=\"color:#a6e22e\">version<\/span> = <span style=\"color:#e6db74\">&#34;1.0&#34;<\/span>, <span style=\"color:#a6e22e\">features<\/span> = [<span style=\"color:#e6db74\">&#34;full&#34;<\/span>] }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># HTML parsing<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">scraper<\/span> = <span style=\"color:#e6db74\">&#34;0.18&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">html5ever<\/span> = <span style=\"color:#e6db74\">&#34;0.26&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Serialization<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">serde<\/span> = { <span style=\"color:#a6e22e\">version<\/span> = <span style=\"color:#e6db74\">&#34;1.0&#34;<\/span>, <span style=\"color:#a6e22e\">features<\/span> = [<span style=\"color:#e6db74\">&#34;derive&#34;<\/span>] }\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">serde_json<\/span> = <span style=\"color:#e6db74\">&#34;1.0&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">csv<\/span> = <span style=\"color:#e6db74\">&#34;1.3&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Configuration<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">config<\/span> = <span style=\"color:#e6db74\">&#34;0.14&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">toml<\/span> = <span style=\"color:#e6db74\">&#34;0.8&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># CLI<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">clap<\/span> = { <span style=\"color:#a6e22e\">version<\/span> = <span style=\"color:#e6db74\">&#34;4.4&#34;<\/span>, <span style=\"color:#a6e22e\">features<\/span> = [<span style=\"color:#e6db74\">&#34;derive&#34;<\/span>] }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Logging<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">log<\/span> = <span style=\"color:#e6db74\">&#34;0.4&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">env_logger<\/span> = <span style=\"color:#e6db74\">&#34;0.10&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Error handling<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">anyhow<\/span> = <span style=\"color:#e6db74\">&#34;1.0&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">thiserror<\/span> = <span style=\"color:#e6db74\">&#34;1.0&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># URL handling<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">url<\/span> = <span style=\"color:#e6db74\">&#34;2.4&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Date\/time<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">chrono<\/span> = { <span style=\"color:#a6e22e\">version<\/span> = <span style=\"color:#e6db74\">&#34;0.4&#34;<\/span>, <span style=\"color:#a6e22e\">features<\/span> = [<span style=\"color:#e6db74\">&#34;serde&#34;<\/span>] }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Random number generation<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">rand<\/span> = <span style=\"color:#e6db74\">&#34;0.8&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>[<span style=\"color:#a6e22e\">dev-dependencies<\/span>]\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"># Testing utilities<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">tokio-test<\/span> = <span style=\"color:#e6db74\">&#34;0.4&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">wiremock<\/span> = <span style=\"color:#e6db74\">&#34;0.5&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">tempfile<\/span> = <span style=\"color:#e6db74\">&#34;3.8&#34;<\/span>\n<\/span><\/span><\/code><\/pre><\/div><\/details>\n<br \/>\n<p>To verify the project setup: run <code>cargo check<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>$ cargo check\n<\/span><\/span><span style=\"display:flex;\"><span>Checking web-scraper v0.1.0 <span style=\"color:#f92672\">(<\/span>\/Users\/ethan\/Projects\/rust-web-scraper<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> Finished <span style=\"color:#e6db74\">`<\/span>dev<span style=\"color:#e6db74\">`<\/span> profile <span style=\"color:#f92672\">[<\/span>unoptimized + debuginfo<span style=\"color:#f92672\">]<\/span> target<span style=\"color:#f92672\">(<\/span>s<span style=\"color:#f92672\">)<\/span> in 1.11s\n<\/span><\/span><\/code><\/pre><\/div><h3 id=\"initial-code\">Initial Code<\/h3>\n<p>At this point, I got to making a detailed requirements document and architecture plan with Claude. Once we settled on a full list of features and an architecture, I had Claude go ahead with the initial code.<\/p>\n<p>I set up the <code>main.rs<\/code> file and main function to be the entrypoint to the scraper which triggers the following operations:<\/p>\n<ol>\n<li>\n<p>Initialize logger and load scraper configuration (<code>config.rs<\/code>)<\/p>\n<\/li>\n<li>\n<p>Initialize output JSON file (where scraped data will be saved)<\/p>\n<\/li>\n<li>\n<p>Load the list of URLs to scrape from a text file<\/p>\n<\/li>\n<li>\n<p>For each URL:<\/p>\n<ul>\n<li>Fetch the HTML content using <code>reqwest<\/code> (<code>fetcher.rs<\/code>)<\/li>\n<li>If the request fails, retry with exponential backoff<\/li>\n<li>If the request succeeds, parse the HTML content to extract the title, date, and content using <code>scraper<\/code> (<code>parser.rs<\/code>)<\/li>\n<li>Save the extracted data to the output JSON file<\/li>\n<li>Increment the success or failure counter as appropriate<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>After all URLs are processed, log a summary of successes and failures, and display the path to the output file<\/p>\n<\/li>\n<\/ol>\n<p>And that&rsquo;s it! Very straightforward.<\/p>\n<details>\n<summary>Expand to view the full `main.rs` file<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ main.rs\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> clap::Parser;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> log::{error, info, warn};\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> std::fs;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> std::path::Path;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> tokio;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> web_scraper::{config, AppConfig, HttpScraper, OutputWriter};\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[derive(Parser)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[command(name = <\/span><span style=\"color:#e6db74\">&#34;web-scraper&#34;<\/span><span style=\"color:#75715e\">)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[command(about = <\/span><span style=\"color:#e6db74\">&#34;A web scraper for technical blog posts&#34;<\/span><span style=\"color:#75715e\">)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">struct<\/span> <span style=\"color:#a6e22e\">Args<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Input file containing URLs (one per line)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> input_file: String,\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Configuration file path\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[arg(short, long, default_value = <\/span><span style=\"color:#e6db74\">&#34;config\/default.toml&#34;<\/span><span style=\"color:#75715e\">)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> config: String,\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Output directory\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[arg(short, long)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> output_dir: Option<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Verbose logging\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[arg(short, long)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> verbose: <span style=\"color:#66d9ef\">bool<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[tokio::main]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">main<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> args <span style=\"color:#f92672\">=<\/span> Args::parse();\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Initialize logger\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> args.verbose {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Enable debug logging only for our crate, keep other crates at info level\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(<span style=\"color:#e6db74\">&#34;web_scraper=debug,info&#34;<\/span>)).init();\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(<span style=\"color:#e6db74\">&#34;info&#34;<\/span>)).init();\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Starting web scraper&#34;<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Input file: {}&#34;<\/span>, args.input_file);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Config file: {}&#34;<\/span>, args.config);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Load configuration\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> config <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">match<\/span> load_configuration(<span style=\"color:#f92672\">&amp;<\/span>args.config) {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(config) <span style=\"color:#f92672\">=&gt;<\/span> config,\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">error!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to load configuration: {}&#34;<\/span>, e);\n<\/span><\/span><span style=\"display:flex;\"><span> std::process::exit(<span style=\"color:#ae81ff\">1<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> };\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Override output directory if specified\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(output_dir) <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">&amp;<\/span>args.output_dir {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Overriding output directory: {}&#34;<\/span>, output_dir);\n<\/span><\/span><span style=\"display:flex;\"><span> config.output.directory <span style=\"color:#f92672\">=<\/span> output_dir.clone();\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Load URLs from input file\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> urls <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">match<\/span> load_urls(<span style=\"color:#f92672\">&amp;<\/span>args.input_file) {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(urls) <span style=\"color:#f92672\">=&gt;<\/span> urls,\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">error!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to load URLs from {}: {}&#34;<\/span>, args.input_file, e);\n<\/span><\/span><span style=\"display:flex;\"><span> std::process::exit(<span style=\"color:#ae81ff\">1<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> };\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Loaded {} URLs to scrape&#34;<\/span>, urls.len());\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Initialize scraper and output writer\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> scraper <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">match<\/span> HttpScraper::new(config.scraper.clone()) {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(scraper) <span style=\"color:#f92672\">=&gt;<\/span> scraper,\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">error!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to initialize scraper: {}&#34;<\/span>, e);\n<\/span><\/span><span style=\"display:flex;\"><span> std::process::exit(<span style=\"color:#ae81ff\">1<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> };\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> output_writer <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">match<\/span> OutputWriter::new(config.output.clone()) {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(writer) <span style=\"color:#f92672\">=&gt;<\/span> writer,\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">error!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to initialize output writer: {}&#34;<\/span>, e);\n<\/span><\/span><span style=\"display:flex;\"><span> std::process::exit(<span style=\"color:#ae81ff\">1<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> };\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Scrape all URLs\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> successful_count <span style=\"color:#f92672\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> failed_count <span style=\"color:#f92672\">=<\/span> <span style=\"color:#ae81ff\">0<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Starting to scrape {} URLs&#34;<\/span>, urls.len());\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> (i, url) <span style=\"color:#66d9ef\">in<\/span> urls.iter().enumerate() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Scraping URL {}\/{}: {}&#34;<\/span>, i <span style=\"color:#f92672\">+<\/span> <span style=\"color:#ae81ff\">1<\/span>, urls.len(), url);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> article <span style=\"color:#f92672\">=<\/span> scraper.scrape_url(url).<span style=\"color:#66d9ef\">await<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> article.success {\n<\/span><\/span><span style=\"display:flex;\"><span> successful_count <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;\u2713 Successfully scraped: {}&#34;<\/span>, url);\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> failed_count <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#ae81ff\">1<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;\u2717 Failed to scrape: {} - {}&#34;<\/span>, url,\n<\/span><\/span><span style=\"display:flex;\"><span> article.error_message.as_deref().unwrap_or(<span style=\"color:#e6db74\">&#34;Unknown error&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Write the article to output files\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Err(e) <span style=\"color:#f92672\">=<\/span> output_writer.write_article(<span style=\"color:#f92672\">&amp;<\/span>article) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">error!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to write article to output: {}&#34;<\/span>, e);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Flush output writer\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Err(e) <span style=\"color:#f92672\">=<\/span> output_writer.flush() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">error!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to flush output: {}&#34;<\/span>, e);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Print summary\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Scraping completed!&#34;<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Successful: {}&#34;<\/span>, successful_count);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Failed: {}&#34;<\/span>, failed_count);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Total: {}&#34;<\/span>, urls.len());\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> success_rate <span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">if<\/span> urls.is_empty() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#ae81ff\">0.0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> (successful_count <span style=\"color:#66d9ef\">as<\/span> <span style=\"color:#66d9ef\">f64<\/span> <span style=\"color:#f92672\">\/<\/span> urls.len() <span style=\"color:#66d9ef\">as<\/span> <span style=\"color:#66d9ef\">f64<\/span>) <span style=\"color:#f92672\">*<\/span> <span style=\"color:#ae81ff\">100.0<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> };\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Success rate: {:.1}%&#34;<\/span>, success_rate);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Show output file locations\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> output_paths <span style=\"color:#f92672\">=<\/span> output_writer.get_output_paths();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Output files written:&#34;<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> path <span style=\"color:#66d9ef\">in<\/span> output_paths {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34; {}&#34;<\/span>, path);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">println!<\/span>(<span style=\"color:#e6db74\">&#34;Web scraper completed successfully!&#34;<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">println!<\/span>(<span style=\"color:#e6db74\">&#34;Successfully scraped: <\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">\/<\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\"> URLs (<\/span><span style=\"color:#e6db74\">{:.1}<\/span><span style=\"color:#e6db74\">%)&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> successful_count, urls.len(), success_rate);\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Load configuration from file, with fallback to default\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">load_configuration<\/span>(config_path: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>AppConfig, Box<span style=\"color:#f92672\">&lt;<\/span><span style=\"color:#66d9ef\">dyn<\/span> std::error::Error<span style=\"color:#f92672\">&gt;&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> Path::new(config_path).exists() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Loading configuration from: {}&#34;<\/span>, config_path);\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(config::load_config(config_path)<span style=\"color:#f92672\">?<\/span>)\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Configuration file not found: {}, using default configuration&#34;<\/span>, config_path);\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(AppConfig::default())\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Load URLs from input file\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">load_urls<\/span>(input_file: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>Vec<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span>, Box<span style=\"color:#f92672\">&lt;<\/span><span style=\"color:#66d9ef\">dyn<\/span> std::error::Error<span style=\"color:#f92672\">&gt;&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Loading URLs from: {}&#34;<\/span>, input_file);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> content <span style=\"color:#f92672\">=<\/span> fs::read_to_string(input_file)<span style=\"color:#f92672\">?<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> urls: Vec<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#f92672\">=<\/span> content\n<\/span><\/span><span style=\"display:flex;\"><span> .lines()\n<\/span><\/span><span style=\"display:flex;\"><span> .map(<span style=\"color:#f92672\">|<\/span>line<span style=\"color:#f92672\">|<\/span> line.trim())\n<\/span><\/span><span style=\"display:flex;\"><span> .filter(<span style=\"color:#f92672\">|<\/span>line<span style=\"color:#f92672\">|<\/span> <span style=\"color:#f92672\">!<\/span>line.is_empty() <span style=\"color:#f92672\">&amp;&amp;<\/span> <span style=\"color:#f92672\">!<\/span>line.starts_with(<span style=\"color:#e6db74\">&#39;#&#39;<\/span>))\n<\/span><\/span><span style=\"display:flex;\"><span> .map(<span style=\"color:#f92672\">|<\/span>line<span style=\"color:#f92672\">|<\/span> line.to_string())\n<\/span><\/span><span style=\"display:flex;\"><span> .collect();\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> urls.is_empty() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> Err(<span style=\"color:#e6db74\">&#34;No valid URLs found in input file&#34;<\/span>.into());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(urls)\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><\/details>\n<br \/>\n<h3 id=\"project-structure\">Project Structure<\/h3>\n<p>The structure follows a conventional Rust project layout:<\/p>\n<pre tabindex=\"0\"><code>web-scraper\/\n\u251c\u2500\u2500 Cargo.toml\n\u251c\u2500\u2500 Cargo.lock\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 config\/\n\u2502 \u2514\u2500\u2500 default.toml\n\u251c\u2500\u2500 src\/\n\u2502 \u251c\u2500\u2500 main.rs\n\u2502 \u251c\u2500\u2500 lib.rs\n\u2502 \u251c\u2500\u2500 config.rs\n\u2502 \u251c\u2500\u2500 scraper.rs\n\u2502 \u251c\u2500\u2500 parser.rs\n\u2502 \u251c\u2500\u2500 output.rs\n\u2502 \u251c\u2500\u2500 models.rs\n\u2502 \u2514\u2500\u2500 error.rs\n\u251c\u2500\u2500 tests\/\n\u2502 \u251c\u2500\u2500 integration_tests.rs\n\u2502 \u251c\u2500\u2500 fixtures\/\n\u2502 \u2502 \u251c\u2500\u2500 sample_urls.txt\n\u2502 \u2502 \u251c\u2500\u2500 expected_output.jsonl\n\u2502 \u2502 \u2514\u2500\u2500 test_pages\/\n\u2502 \u2514\u2500\u2500 mock_server.rs\n\u251c\u2500\u2500 examples\/\n\u2502 \u251c\u2500\u2500 sample_input.txt\n\u2502 \u2514\u2500\u2500 sample_config.toml\n\u2514\u2500\u2500 target\/\n<\/code><\/pre><h3 id=\"handling-robotstxt\">Handling robots.txt<\/h3>\n<p>One of the well-known conventions of web scraping is to respect the <code>robots.txt<\/code> file for rules on scraping websites.<\/p>\n<p>I implemented this by with a simple <code>robots.rs<\/code> module that fetches the text file, parses it line by line into key-value pairs, and then builds a set of rules based on user agents and allowed\/disallowed paths. Function preview here:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ robots.rs\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">impl<\/span> SimpleRobots {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">parse<\/span>(content: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; <span style=\"color:#a6e22e\">Self<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> rules <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> current_user_agents: Vec<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> allowed_paths <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> disallowed_paths <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> crawl_delay <span style=\"color:#f92672\">=<\/span> None;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> line <span style=\"color:#66d9ef\">in<\/span> content.lines() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Loop through file line by line\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> line <span style=\"color:#f92672\">=<\/span> line.trim();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> line.is_empty() <span style=\"color:#f92672\">||<\/span> line.starts_with(<span style=\"color:#e6db74\">&#39;#&#39;<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">continue<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Split on `:`\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some((key, value)) <span style=\"color:#f92672\">=<\/span> line.split_once(<span style=\"color:#e6db74\">&#39;:&#39;<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> key <span style=\"color:#f92672\">=<\/span> key.trim().to_lowercase();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> value <span style=\"color:#f92672\">=<\/span> value.trim();\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> key.as_str() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;user-agent&#34;<\/span> <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Save previous rule if any\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">!<\/span>current_user_agents.is_empty() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> ua <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>current_user_agents {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Add a new rule for user agent\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> rules.push(RobotRule {\n<\/span><\/span><span style=\"display:flex;\"><span> user_agent: <span style=\"color:#a6e22e\">ua<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> allowed_paths: <span style=\"color:#a6e22e\">allowed_paths<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> disallowed_paths: <span style=\"color:#a6e22e\">disallowed_paths<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> crawl_delay,\n<\/span><\/span><span style=\"display:flex;\"><span> });\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ ... remaining rules and parsing logic ...\n<\/span><\/span><\/span><\/code><\/pre><\/div><p>Then, we run the <code>is_allowed<\/code> function to check whether the given URL is permitted based on our current rules.<\/p>\n<details>\n<summary>Expand to view full robots.rs file<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> <span style=\"color:#66d9ef\">crate<\/span>::error::ScraperError;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> log::{debug, info, warn};\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> reqwest::Client;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> std::collections::HashMap;\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> std::time::{Duration, Instant};\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">use<\/span> url::Url;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Simple robots.txt rule\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[derive(Debug, Clone)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">struct<\/span> <span style=\"color:#a6e22e\">RobotRule<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> user_agent: String,\n<\/span><\/span><span style=\"display:flex;\"><span> allowed_paths: Vec<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> disallowed_paths: Vec<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> crawl_delay: Option<span style=\"color:#f92672\">&lt;<\/span><span style=\"color:#66d9ef\">f64<\/span><span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Simple robots.txt parser result\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[derive(Debug, Clone)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">struct<\/span> <span style=\"color:#a6e22e\">SimpleRobots<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> rules: Vec<span style=\"color:#f92672\">&lt;<\/span>RobotRule<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">impl<\/span> SimpleRobots {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Parse robots.txt content\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">parse<\/span>(content: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; <span style=\"color:#a6e22e\">Self<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> rules <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> current_user_agents: Vec<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> allowed_paths <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> disallowed_paths <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> crawl_delay <span style=\"color:#f92672\">=<\/span> None;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> line <span style=\"color:#66d9ef\">in<\/span> content.lines() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> line <span style=\"color:#f92672\">=<\/span> line.trim();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> line.is_empty() <span style=\"color:#f92672\">||<\/span> line.starts_with(<span style=\"color:#e6db74\">&#39;#&#39;<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">continue<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some((key, value)) <span style=\"color:#f92672\">=<\/span> line.split_once(<span style=\"color:#e6db74\">&#39;:&#39;<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> key <span style=\"color:#f92672\">=<\/span> key.trim().to_lowercase();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> value <span style=\"color:#f92672\">=<\/span> value.trim();\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> key.as_str() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;user-agent&#34;<\/span> <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Save previous rule if any\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">!<\/span>current_user_agents.is_empty() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> ua <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>current_user_agents {\n<\/span><\/span><span style=\"display:flex;\"><span> rules.push(RobotRule {\n<\/span><\/span><span style=\"display:flex;\"><span> user_agent: <span style=\"color:#a6e22e\">ua<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> allowed_paths: <span style=\"color:#a6e22e\">allowed_paths<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> disallowed_paths: <span style=\"color:#a6e22e\">disallowed_paths<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> crawl_delay,\n<\/span><\/span><span style=\"display:flex;\"><span> });\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Reset for new user-agent block\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> current_user_agents <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">vec!<\/span>[value.to_string()];\n<\/span><\/span><span style=\"display:flex;\"><span> allowed_paths.clear();\n<\/span><\/span><span style=\"display:flex;\"><span> disallowed_paths.clear();\n<\/span><\/span><span style=\"display:flex;\"><span> crawl_delay <span style=\"color:#f92672\">=<\/span> None;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;allow&#34;<\/span> <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> allowed_paths.push(value.to_string());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;disallow&#34;<\/span> <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> disallowed_paths.push(value.to_string());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;crawl-delay&#34;<\/span> <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Ok(delay) <span style=\"color:#f92672\">=<\/span> value.parse::<span style=\"color:#f92672\">&lt;<\/span><span style=\"color:#66d9ef\">f64<\/span><span style=\"color:#f92672\">&gt;<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span> crawl_delay <span style=\"color:#f92672\">=<\/span> Some(delay);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> _ <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Ignore unknown directives\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Save the last rule\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">!<\/span>current_user_agents.is_empty() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> ua <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>current_user_agents {\n<\/span><\/span><span style=\"display:flex;\"><span> rules.push(RobotRule {\n<\/span><\/span><span style=\"display:flex;\"><span> user_agent: <span style=\"color:#a6e22e\">ua<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> allowed_paths: <span style=\"color:#a6e22e\">allowed_paths<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> disallowed_paths: <span style=\"color:#a6e22e\">disallowed_paths<\/span>.clone(),\n<\/span><\/span><span style=\"display:flex;\"><span> crawl_delay,\n<\/span><\/span><span style=\"display:flex;\"><span> });\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> Self { rules }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Check if a path is allowed for a user agent\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">is_allowed<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self, user_agent: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>, path: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; <span style=\"color:#66d9ef\">bool<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Find applicable rules (exact match or wildcard)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> applicable_rules: Vec<span style=\"color:#f92672\">&lt;<\/span>_<span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#f92672\">=<\/span> self.rules\n<\/span><\/span><span style=\"display:flex;\"><span> .iter()\n<\/span><\/span><span style=\"display:flex;\"><span> .filter(<span style=\"color:#f92672\">|<\/span>rule<span style=\"color:#f92672\">|<\/span> rule.user_agent <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;*&#34;<\/span> <span style=\"color:#f92672\">||<\/span> rule.user_agent <span style=\"color:#f92672\">==<\/span> user_agent)\n<\/span><\/span><span style=\"display:flex;\"><span> .collect();\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> applicable_rules.is_empty() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#66d9ef\">true<\/span>; <span style=\"color:#75715e\">\/\/ No rules found, allow access\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check rules in order of specificity (specific user-agent before wildcard)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> sorted_rules <span style=\"color:#f92672\">=<\/span> applicable_rules;\n<\/span><\/span><span style=\"display:flex;\"><span> sorted_rules.sort_by(<span style=\"color:#f92672\">|<\/span>a, b<span style=\"color:#f92672\">|<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> a.user_agent <span style=\"color:#f92672\">==<\/span> user_agent <span style=\"color:#f92672\">&amp;&amp;<\/span> b.user_agent <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;*&#34;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> std::cmp::Ordering::Less\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> <span style=\"color:#66d9ef\">if<\/span> a.user_agent <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;*&#34;<\/span> <span style=\"color:#f92672\">&amp;&amp;<\/span> b.user_agent <span style=\"color:#f92672\">==<\/span> user_agent {\n<\/span><\/span><span style=\"display:flex;\"><span> std::cmp::Ordering::Greater\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> std::cmp::Ordering::Equal\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> });\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> rule <span style=\"color:#66d9ef\">in<\/span> sorted_rules {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check if explicitly allowed\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> allowed <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>rule.allowed_paths {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> path.starts_with(allowed) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#66d9ef\">true<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check if disallowed\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> disallowed <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>rule.disallowed_paths {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> disallowed.is_empty() <span style=\"color:#f92672\">||<\/span> path.starts_with(disallowed) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#66d9ef\">false<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">true<\/span> <span style=\"color:#75715e\">\/\/ Default to allow if no specific rules match\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Get crawl delay for a user agent\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">get_crawl_delay<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self, user_agent: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Option<span style=\"color:#f92672\">&lt;<\/span><span style=\"color:#66d9ef\">f64<\/span><span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Look for specific user-agent first, then wildcard\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> rule <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>self.rules {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> rule.user_agent <span style=\"color:#f92672\">==<\/span> user_agent {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> rule.crawl_delay;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> rule <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#f92672\">&amp;<\/span>self.rules {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> rule.user_agent <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;*&#34;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> rule.crawl_delay;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> None\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Cache entry for robots.txt data\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[derive(Debug, Clone)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">struct<\/span> <span style=\"color:#a6e22e\">RobotsCacheEntry<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> robots_txt: Option<span style=\"color:#f92672\">&lt;<\/span>SimpleRobots<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> cached_at: <span style=\"color:#a6e22e\">Instant<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Robots.txt compliance checker with caching\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">struct<\/span> <span style=\"color:#a6e22e\">RobotsChecker<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> client: <span style=\"color:#a6e22e\">Client<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> cache: <span style=\"color:#a6e22e\">HashMap<\/span><span style=\"color:#f92672\">&lt;<\/span>String, RobotsCacheEntry<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> cache_duration: <span style=\"color:#a6e22e\">Duration<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> user_agent: String,\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">impl<\/span> RobotsChecker {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Create a new robots.txt checker\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">new<\/span>(client: <span style=\"color:#a6e22e\">Client<\/span>, user_agent: String) -&gt; <span style=\"color:#a6e22e\">Self<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> Self {\n<\/span><\/span><span style=\"display:flex;\"><span> client,\n<\/span><\/span><span style=\"display:flex;\"><span> cache: <span style=\"color:#a6e22e\">HashMap<\/span>::new(),\n<\/span><\/span><span style=\"display:flex;\"><span> cache_duration: <span style=\"color:#a6e22e\">Duration<\/span>::from_secs(<span style=\"color:#ae81ff\">3600<\/span>), <span style=\"color:#75715e\">\/\/ Cache for 1 hour\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> user_agent,\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Check if scraping is allowed for the given URL\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">is_allowed<\/span>(<span style=\"color:#f92672\">&amp;<\/span><span style=\"color:#66d9ef\">mut<\/span> self, url: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span><span style=\"color:#66d9ef\">bool<\/span>, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> parsed_url <span style=\"color:#f92672\">=<\/span> Url::parse(url).map_err(<span style=\"color:#f92672\">|<\/span>e<span style=\"color:#f92672\">|<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> ScraperError::ParseError(<span style=\"color:#a6e22e\">format!<\/span>(<span style=\"color:#e6db74\">&#34;Invalid URL <\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">: <\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">&#34;<\/span>, url, e))\n<\/span><\/span><span style=\"display:flex;\"><span> })<span style=\"color:#f92672\">?<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> base_url <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">format!<\/span>(<span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">:\/\/<\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">&#34;<\/span>, parsed_url.scheme(), parsed_url.host_str().unwrap_or(<span style=\"color:#e6db74\">&#34;&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> robots_url <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">format!<\/span>(<span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">\/robots.txt&#34;<\/span>, base_url);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check cache first\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(entry) <span style=\"color:#f92672\">=<\/span> self.cache.get(<span style=\"color:#f92672\">&amp;<\/span>base_url) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> entry.cached_at.elapsed() <span style=\"color:#f92672\">&lt;<\/span> self.cache_duration {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Using cached robots.txt for {}&#34;<\/span>, base_url);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> Ok(self.check_permission(entry, <span style=\"color:#f92672\">&amp;<\/span>parsed_url));\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Fetch robots.txt\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">info!<\/span>(<span style=\"color:#e6db74\">&#34;Fetching robots.txt from {}&#34;<\/span>, robots_url);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> robots_txt <span style=\"color:#f92672\">=<\/span> self.fetch_robots_txt(<span style=\"color:#f92672\">&amp;<\/span>robots_url).<span style=\"color:#66d9ef\">await<\/span><span style=\"color:#f92672\">?<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Cache the result\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> entry <span style=\"color:#f92672\">=<\/span> RobotsCacheEntry {\n<\/span><\/span><span style=\"display:flex;\"><span> robots_txt,\n<\/span><\/span><span style=\"display:flex;\"><span> cached_at: <span style=\"color:#a6e22e\">Instant<\/span>::now(),\n<\/span><\/span><span style=\"display:flex;\"><span> };\n<\/span><\/span><span style=\"display:flex;\"><span> self.cache.insert(base_url, entry.clone());\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(self.check_permission(<span style=\"color:#f92672\">&amp;<\/span>entry, <span style=\"color:#f92672\">&amp;<\/span>parsed_url))\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Get crawl delay for the given URL (in seconds)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">get_crawl_delay<\/span>(<span style=\"color:#f92672\">&amp;<\/span><span style=\"color:#66d9ef\">mut<\/span> self, url: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>Option<span style=\"color:#f92672\">&lt;<\/span>Duration<span style=\"color:#f92672\">&gt;<\/span>, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> parsed_url <span style=\"color:#f92672\">=<\/span> Url::parse(url).map_err(<span style=\"color:#f92672\">|<\/span>e<span style=\"color:#f92672\">|<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> ScraperError::ParseError(<span style=\"color:#a6e22e\">format!<\/span>(<span style=\"color:#e6db74\">&#34;Invalid URL <\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">: <\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">&#34;<\/span>, url, e))\n<\/span><\/span><span style=\"display:flex;\"><span> })<span style=\"color:#f92672\">?<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> base_url <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">format!<\/span>(<span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">:\/\/<\/span><span style=\"color:#e6db74\">{}<\/span><span style=\"color:#e6db74\">&#34;<\/span>, parsed_url.scheme(), parsed_url.host_str().unwrap_or(<span style=\"color:#e6db74\">&#34;&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check cache first\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(entry) <span style=\"color:#f92672\">=<\/span> self.cache.get(<span style=\"color:#f92672\">&amp;<\/span>base_url) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> entry.cached_at.elapsed() <span style=\"color:#f92672\">&lt;<\/span> self.cache_duration {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(<span style=\"color:#66d9ef\">ref<\/span> robots) <span style=\"color:#f92672\">=<\/span> entry.robots_txt {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(delay) <span style=\"color:#f92672\">=<\/span> robots.get_crawl_delay(<span style=\"color:#f92672\">&amp;<\/span>self.user_agent) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> Ok(Some(Duration::from_secs_f64(delay)));\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> Ok(None);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ If not in cache, trigger a fetch by calling is_allowed\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> self.is_allowed(url).<span style=\"color:#66d9ef\">await<\/span><span style=\"color:#f92672\">?<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Now check again\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(entry) <span style=\"color:#f92672\">=<\/span> self.cache.get(<span style=\"color:#f92672\">&amp;<\/span>base_url) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(<span style=\"color:#66d9ef\">ref<\/span> robots) <span style=\"color:#f92672\">=<\/span> entry.robots_txt {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(delay) <span style=\"color:#f92672\">=<\/span> robots.get_crawl_delay(<span style=\"color:#f92672\">&amp;<\/span>self.user_agent) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> Ok(Some(Duration::from_secs_f64(delay)));\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(None)\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Fetch robots.txt from the given URL\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">fetch_robots_txt<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self, robots_url: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>Option<span style=\"color:#f92672\">&lt;<\/span>SimpleRobots<span style=\"color:#f92672\">&gt;<\/span>, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> self.client.get(robots_url).send().<span style=\"color:#66d9ef\">await<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(response) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> status <span style=\"color:#f92672\">=<\/span> response.status();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Robots.txt response status for {}: {}&#34;<\/span>, robots_url, status);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> status.is_success() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> response.text().<span style=\"color:#66d9ef\">await<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(content) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Successfully fetched robots.txt from {}&#34;<\/span>, robots_url);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> robots <span style=\"color:#f92672\">=<\/span> SimpleRobots::parse(<span style=\"color:#f92672\">&amp;<\/span>content);\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(Some(robots))\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to read robots.txt content from {}: {}&#34;<\/span>, robots_url, e);\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(None)\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> <span style=\"color:#66d9ef\">if<\/span> status <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">404<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;No robots.txt found at {} (404), allowing access&#34;<\/span>, robots_url);\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(None)\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Unexpected status {} for robots.txt at {}&#34;<\/span>, status, robots_url);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Be conservative - if we can&#39;t fetch robots.txt, allow access\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> Ok(None)\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Network error fetching robots.txt from {}: {}&#34;<\/span>, robots_url, e);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Network errors should not block scraping completely\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> Ok(None)\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Check if the URL is allowed based on robots.txt rules\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">check_permission<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self, entry: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#a6e22e\">RobotsCacheEntry<\/span>, url: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#a6e22e\">Url<\/span>) -&gt; <span style=\"color:#66d9ef\">bool<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#66d9ef\">let<\/span> Some(<span style=\"color:#66d9ef\">ref<\/span> robots) <span style=\"color:#f92672\">=<\/span> entry.robots_txt {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> path <span style=\"color:#f92672\">=<\/span> url.path();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> allowed <span style=\"color:#f92672\">=<\/span> robots.is_allowed(<span style=\"color:#f92672\">&amp;<\/span>self.user_agent, path);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Robots.txt check for {} path &#39;{}&#39;: {}&#34;<\/span>, url.host_str().unwrap_or(<span style=\"color:#e6db74\">&#34;unknown&#34;<\/span>), path,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> allowed { <span style=\"color:#e6db74\">&#34;ALLOWED&#34;<\/span> } <span style=\"color:#66d9ef\">else<\/span> { <span style=\"color:#e6db74\">&#34;DISALLOWED&#34;<\/span> });\n<\/span><\/span><span style=\"display:flex;\"><span> allowed\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;No robots.txt rules found, allowing access to {}&#34;<\/span>, url);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Clear the cache (useful for testing)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">clear_cache<\/span>(<span style=\"color:#f92672\">&amp;<\/span><span style=\"color:#66d9ef\">mut<\/span> self) {\n<\/span><\/span><span style=\"display:flex;\"><span> self.cache.clear();\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Get cache statistics for debugging\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">cache_stats<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self) -&gt; (<span style=\"color:#66d9ef\">usize<\/span>, <span style=\"color:#66d9ef\">usize<\/span>) {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> total <span style=\"color:#f92672\">=<\/span> self.cache.len();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> expired <span style=\"color:#f92672\">=<\/span> self.cache.values()\n<\/span><\/span><span style=\"display:flex;\"><span> .filter(<span style=\"color:#f92672\">|<\/span>entry<span style=\"color:#f92672\">|<\/span> entry.cached_at.elapsed() <span style=\"color:#f92672\">&gt;=<\/span> self.cache_duration)\n<\/span><\/span><span style=\"display:flex;\"><span> .count();\n<\/span><\/span><span style=\"display:flex;\"><span> (total, expired)\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">#[cfg(test)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">mod<\/span> tests {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">use<\/span> <span style=\"color:#66d9ef\">super<\/span>::<span style=\"color:#f92672\">*<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[tokio::test]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">test_robots_checker_creation<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> client <span style=\"color:#f92672\">=<\/span> Client::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> checker <span style=\"color:#f92672\">=<\/span> RobotsChecker::new(client, <span style=\"color:#e6db74\">&#34;test-agent\/1.0&#34;<\/span>.to_string());\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert_eq!<\/span>(checker.cache.len(), <span style=\"color:#ae81ff\">0<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[tokio::test]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">test_cache_stats<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> client <span style=\"color:#f92672\">=<\/span> Client::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> checker <span style=\"color:#f92672\">=<\/span> RobotsChecker::new(client, <span style=\"color:#e6db74\">&#34;test-agent\/1.0&#34;<\/span>.to_string());\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> (total, expired) <span style=\"color:#f92672\">=<\/span> checker.cache_stats();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert_eq!<\/span>(total, <span style=\"color:#ae81ff\">0<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert_eq!<\/span>(expired, <span style=\"color:#ae81ff\">0<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[tokio::test]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">test_clear_cache<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> client <span style=\"color:#f92672\">=<\/span> Client::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> checker <span style=\"color:#f92672\">=<\/span> RobotsChecker::new(client, <span style=\"color:#e6db74\">&#34;test-agent\/1.0&#34;<\/span>.to_string());\n<\/span><\/span><span style=\"display:flex;\"><span> checker.clear_cache();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert_eq!<\/span>(checker.cache.len(), <span style=\"color:#ae81ff\">0<\/span>);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">#[test]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">test_simple_robots_parsing<\/span>() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> content <span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">r<\/span><span style=\"color:#e6db74\">#&#34;\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">User-agent: *\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">Disallow: \/private\/\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">Allow: \/public\/\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">Crawl-delay: 1\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">User-agent: test-bot\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">Disallow: \/\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">&#34;#<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> robots <span style=\"color:#f92672\">=<\/span> SimpleRobots::parse(content);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Should disallow everything for test-bot\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert!<\/span>(<span style=\"color:#f92672\">!<\/span>robots.is_allowed(<span style=\"color:#e6db74\">&#34;test-bot&#34;<\/span>, <span style=\"color:#e6db74\">&#34;\/anything&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Should allow \/public\/ but disallow \/private\/ for other bots\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert!<\/span>(<span style=\"color:#f92672\">!<\/span>robots.is_allowed(<span style=\"color:#e6db74\">&#34;other-bot&#34;<\/span>, <span style=\"color:#e6db74\">&#34;\/private\/test&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert!<\/span>(robots.is_allowed(<span style=\"color:#e6db74\">&#34;other-bot&#34;<\/span>, <span style=\"color:#e6db74\">&#34;\/public\/test&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert!<\/span>(robots.is_allowed(<span style=\"color:#e6db74\">&#34;other-bot&#34;<\/span>, <span style=\"color:#e6db74\">&#34;\/random&#34;<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Should get crawl delay for wildcard\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert_eq!<\/span>(robots.get_crawl_delay(<span style=\"color:#e6db74\">&#34;other-bot&#34;<\/span>), Some(<span style=\"color:#ae81ff\">1.0<\/span>));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">assert_eq!<\/span>(robots.get_crawl_delay(<span style=\"color:#e6db74\">&#34;test-bot&#34;<\/span>), None);\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><\/details>\n<br \/>\n<h3 id=\"fetching-and-parsing\">Fetching and Parsing<\/h3>\n<p>The fetching uses the <code>reqwest<\/code> library to make HTTP GET requests.<\/p>\n<p>Upon failures, the code runs standard exponential backoff retry logic, up to the configured maximum retries. Here&rsquo;s the relevant function:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span><span style=\"color:#e6db74\">\/\/\/ Fetch URL content with retry logic\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">async<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">fetch_with_retry<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self, url: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#a6e22e\">Url<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>String, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> last_error <span style=\"color:#f92672\">=<\/span> None;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> attempt <span style=\"color:#66d9ef\">in<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">..=<\/span>self.config.max_retries {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Attempt {} to fetch {}&#34;<\/span>, attempt, url);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> self.client.get(url.as_str()).send().<span style=\"color:#66d9ef\">await<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(response) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> status <span style=\"color:#f92672\">=<\/span> response.status();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Response status for {}: {}&#34;<\/span>, url, status);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> status.is_success() {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> response.text().<span style=\"color:#66d9ef\">await<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(text) <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#66d9ef\">return<\/span> Ok(text),\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> last_error <span style=\"color:#f92672\">=<\/span> Some(ScraperError::HttpError(e));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to read response text on attempt {}: {}&#34;<\/span>, attempt, last_error.as_ref().unwrap());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> } <span style=\"color:#66d9ef\">else<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> error <span style=\"color:#f92672\">=<\/span> ScraperError::HttpError(reqwest::Error::from(\n<\/span><\/span><span style=\"display:flex;\"><span> reqwest::Error::from(response.error_for_status().unwrap_err()),\n<\/span><\/span><span style=\"display:flex;\"><span> ));\n<\/span><\/span><span style=\"display:flex;\"><span> last_error <span style=\"color:#f92672\">=<\/span> Some(error);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;HTTP error on attempt {}: {}&#34;<\/span>, attempt, last_error.as_ref().unwrap());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> last_error <span style=\"color:#f92672\">=<\/span> Some(ScraperError::HttpError(e));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Request failed on attempt {}: {}&#34;<\/span>, attempt, last_error.as_ref().unwrap());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Add exponential backoff delay between retries\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> attempt <span style=\"color:#f92672\">&lt;<\/span> self.config.max_retries {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> delay <span style=\"color:#f92672\">=<\/span> Duration::from_millis(<span style=\"color:#ae81ff\">1000<\/span> <span style=\"color:#f92672\">*<\/span> (<span style=\"color:#ae81ff\">2_<\/span><span style=\"color:#66d9ef\">u64<\/span>.pow(attempt <span style=\"color:#f92672\">-<\/span> <span style=\"color:#ae81ff\">1<\/span>)));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">debug!<\/span>(<span style=\"color:#e6db74\">&#34;Waiting {}ms before retry&#34;<\/span>, delay.as_millis());\n<\/span><\/span><span style=\"display:flex;\"><span> sleep(delay).<span style=\"color:#66d9ef\">await<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><\/code><\/pre><\/div><p>The parsing uses the <code>scraper<\/code> library to parse HTML and extract the title, date, and main content.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Parse HTML content and extract article information\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">parse<\/span>(<span style=\"color:#f92672\">&amp;<\/span>self, html: <span style=\"color:#66d9ef\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>ParsedContent, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> document <span style=\"color:#f92672\">=<\/span> Html::parse_document(html);\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> title <span style=\"color:#f92672\">=<\/span> self.extract_title(<span style=\"color:#f92672\">&amp;<\/span>document);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> author <span style=\"color:#f92672\">=<\/span> self.extract_author(<span style=\"color:#f92672\">&amp;<\/span>document);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> publication_date <span style=\"color:#f92672\">=<\/span> self.extract_date(<span style=\"color:#f92672\">&amp;<\/span>document);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> content <span style=\"color:#f92672\">=<\/span> self.extract_content(<span style=\"color:#f92672\">&amp;<\/span>document)<span style=\"color:#f92672\">?<\/span>;\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(ParsedContent {\n<\/span><\/span><span style=\"display:flex;\"><span> title,\n<\/span><\/span><span style=\"display:flex;\"><span> author,\n<\/span><\/span><span style=\"display:flex;\"><span> publication_date,\n<\/span><\/span><span style=\"display:flex;\"><span> content,\n<\/span><\/span><span style=\"display:flex;\"><span> })\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><\/code><\/pre><\/div><p>To find the title, we have to deal with the different styles of specifying layouts across the web. Here&rsquo;s how that is set up:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ parser.rs\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">create_title_selectors<\/span>() -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>Vec<span style=\"color:#f92672\">&lt;<\/span>Selector<span style=\"color:#f92672\">&gt;<\/span>, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> selectors <span style=\"color:#f92672\">=<\/span> <span style=\"color:#a6e22e\">vec!<\/span>[\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;article h1&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;h1.title&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;h1.post-title&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;h1.entry-title&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;.post-header h1&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;.article-header h1&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;[property=<\/span><span style=\"color:#ae81ff\">\\&#34;<\/span><span style=\"color:#e6db74\">og:title<\/span><span style=\"color:#ae81ff\">\\&#34;<\/span><span style=\"color:#e6db74\">]&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;title&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;h1&#34;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> ];\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> Self::parse_selectors(<span style=\"color:#f92672\">&amp;<\/span>selectors)\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><p>We use similar logic to extract the author and content as well. The <code>parse_selectors<\/code> simply takes in these selectors and matches it against the document:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">\/\/\/ Parse a list of CSS selectors\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">fn<\/span> <span style=\"color:#a6e22e\">parse_selectors<\/span>(selectors: <span style=\"color:#66d9ef\">&amp;<\/span>[<span style=\"color:#f92672\">&amp;<\/span><span style=\"color:#66d9ef\">str<\/span>]) -&gt; Result<span style=\"color:#f92672\">&lt;<\/span>Vec<span style=\"color:#f92672\">&lt;<\/span>Selector<span style=\"color:#f92672\">&gt;<\/span>, ScraperError<span style=\"color:#f92672\">&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">let<\/span> <span style=\"color:#66d9ef\">mut<\/span> parsed <span style=\"color:#f92672\">=<\/span> Vec::new();\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> selector_str <span style=\"color:#66d9ef\">in<\/span> selectors {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> Selector::parse(selector_str) {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(selector) <span style=\"color:#f92672\">=&gt;<\/span> parsed.push(selector),\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Invalid selector &#39;{}&#39;: {:?}&#34;<\/span>, selector_str, e);\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Continue with other selectors instead of failing\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(parsed)\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><\/code><\/pre><\/div><p>Once the content is parsed, we return a <code>ParsedContent<\/code> struct with the extracted fields, which gets saved into the output JSON file.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">pub<\/span> <span style=\"color:#66d9ef\">struct<\/span> <span style=\"color:#a6e22e\">ParsedContent<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> title: Option<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> author: Option<span style=\"color:#f92672\">&lt;<\/span>String<span style=\"color:#f92672\">&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> publication_date: Option<span style=\"color:#f92672\">&lt;<\/span>DateTime<span style=\"color:#f92672\">&lt;<\/span>Utc<span style=\"color:#f92672\">&gt;&gt;<\/span>,\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">pub<\/span> content: String,\n<\/span><\/span><span style=\"display:flex;\"><span>}\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"results\">Results<\/h2>\n<p>I ran the scraper on a list of 105 technical blog posts, all related to Scala.<\/p>\n<p>With the default configuration, and without even implementing concurrent processing, the scraper completed in 7 minutes. It was able successfuly scrape data on 56 out of the 105 URLs, for a success rate of 53.3%.<\/p>\n<p><img src=\"image.png\" alt=\"alt text\"><\/p>\n<p>From a quick poke through, it looked like most of the failures were due to URLs which no longer exist. This also slows down the scraper because of the retry delays. We could improve this dramatically by simply cleaning up the input URL list, and of course by adding concurrency as well.<\/p>\n<h2 id=\"takeaways\">Takeaways<\/h2>\n<h3 id=\"syntax\">Syntax<\/h3>\n<p>I generally am liking Rust&rsquo;s syntax so far. It feels like a mixture of Go, for the error handling, and Scala for the semi-functional style and pattern matching.<\/p>\n<p>I particularly liked the <code>match<\/code> statement for handling errors, such as in getting responses:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-rust\" data-lang=\"rust\"><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">match<\/span> response.text().<span style=\"color:#66d9ef\">await<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> Ok(text) <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#66d9ef\">return<\/span> Ok(text),\n<\/span><\/span><span style=\"display:flex;\"><span> Err(e) <span style=\"color:#f92672\">=&gt;<\/span> {\n<\/span><\/span><span style=\"display:flex;\"><span> last_error <span style=\"color:#f92672\">=<\/span> Some(ScraperError::HttpError(e));\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">warn!<\/span>(<span style=\"color:#e6db74\">&#34;Failed to read response text on attempt {}: {}&#34;<\/span>, attempt, last_error.as_ref().unwrap());\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><span style=\"display:flex;\"><span> }\n<\/span><\/span><\/code><\/pre><\/div><p>The usage of <code>Some()<\/code> also reminds me of Scala&rsquo;s safe style of handling optional values, in a good way.<\/p>\n<p>The type annotations are very reasonable, and VS code seems to have excellent support for showing types, docstrings, and function signatures based on the context.<\/p>\n<h3 id=\"project-setup\">Project Setup<\/h3>\n<p>The Rust project setup is very clean. I&rsquo;ve heard awesome things about Cargo and it does not disappoint. Everything just feels like a more refined version of the build systems I&rsquo;m used to: it&rsquo;s like a cleaner and faster version of Poetry (Python) combined with a smoother and more integrated version of sbt for Scala.<\/p>\n<h3 id=\"performance-and-async\">Performance and Async<\/h3>\n<p>Of course, performance is one of the big selling points of Rust. This project isn&rsquo;t near large enough for performance to be a concern yet, but it has felt very snappy so far in both tests and development runs.<\/p>\n<p>The Async support with <code>tokio<\/code> has been very nice as well. The async\/await syntax feels similar to Python&rsquo;s, but the experience is just smooth and easy compared to the rough edges with Python.<\/p>\n<h3 id=\"advanced-language-features\">Advanced Language Features<\/h3>\n<p>With all that said, I&rsquo;m yet to experience the real power of Rust that I&rsquo;ve read about. In the future, I&rsquo;ll definitely want to explore how Rust handles memory safety, ownership, concurrency, and more.<\/p>\n<p>I plan to expand on this project somewhat going forward, but I&rsquo;m even more excited to start working on Apache DataFusion, which is a data processing engine written in Rust.<\/p>\n<h2 id=\"next-steps\">Next Steps<\/h2>\n<ul>\n<li>Add concurrency with <code>tokio<\/code> tasks to speed up scraping<\/li>\n<li>Implement true web crawling with link extraction and breadth-first traversal<\/li>\n<li>Add persistent, queryable storage for parsed contents with SQLite or Postgres<\/li>\n<li>Improve error handling and logging with more granular error types, such as invalid URLs, blocked by robots.txt, parsing failures, etc.<\/li>\n<\/ul>\n<p>Hope you enjoyed! Again, code is up on <a href=\"https:\/\/github.com\/ewoodbury\/rust-web-scraper\" target=\"_blank\" rel=\"noopener\">Github<\/a> for you to poke through.<\/p>"},{"title":"A Clean Spark Project Template for Scala","link":"https:\/\/ewoodbury.com\/posts\/2025-09-07_spark_template_scala_213\/","pubDate":"Sun, 07 Sep 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-09-07_spark_template_scala_213\/","description":"<p>New software projects can be tricky to set up, especially if it&rsquo;s for an unfamiliar language or framework. Early on, a few poor decisions can cause issues that stick around for years.<\/p>\n<p>When I started a new Spark project with Scala at work, it took me a couple of weeks to get the build system, test framework, and core data pipeline structure set up in a way I was happy with.<\/p>\n<p>I think there&rsquo;s tremendous value in sharing project templates which have best practices baked in. I think this is especially helpful for Spark and Scala because:<\/p>\n<ul>\n<li>Spark is complex, and it&rsquo;s not clear how to go from Jupyter notebooks (where many Spark developers start) to a full, production-ready job.<\/li>\n<li>Scala is lacks the &ldquo;Getting Started&rdquo; resources that exist for data engineering in Python. We as a community should do a better job of building an easy on-ramp for folks new to this domain.<\/li>\n<li>Testing remains sorely neglected in data engineering. It&rsquo;s hugely beneficial to have a test framework running from the get-go. Spark also makes it even easier to test compared to other databases, since you can run everything on your machine in local mode.<\/li>\n<\/ul>\n<h2 id=\"why-scala\">Why Scala?<\/h2>\n<p>For Spark projects, I believe Scala has two main advantages over PySpark:<\/p>\n<ol>\n<li>Type Safety: With the Scala API for Spark, you have <code>DataSet<\/code> objects, which let you specify the columns in every table throughout the job. This lets you keep track of the fields in complex jobs at each transformation step, compared to they typical PySpark\/pandas style of lacking dataframe information until runtime. Besides this, Scala&rsquo;s status as a compiled language catches many other common mistakes immediately, without requiring a slow build and deploy cycle to test.<\/li>\n<li>Functional Programming by default: This aspect is more subtle, but pays off in the long run. Scala encourages immutability of data structures and pure functions. There are even common Scala utilities (like <a href=\"https:\/\/www.wartremover.org\/\" target=\"_blank\" rel=\"noopener\">WartRemover<\/a>) that help enforce this. Functional programming leads to code that is easier to reason about, test, and maintain. Of course, this is possible in Python as well, but Scala makes this the default style.<\/li>\n<\/ol>\n<p>Note that performance is not included here, despite being a common justification. All Spark code is transpiled to Spark operations, so PySpark versus Scala job definitions usually have identical performance. There are some small benefits with UDFs in Scala, but that&rsquo;s less common (and might indicate you&rsquo;re doing something wrong).<\/p>\n<p>I plan to write a longer post on Scala for data engineering in the future!<\/p>\n<p>Below, I share a Spark project template with Scala 2.13 that from what I&rsquo;ve learned running massive Spark pipelines in production. The template is available on <a href=\"https:\/\/github.com\/ewoodbury\/spark-template-scala-2.13\" target=\"_blank\" rel=\"noopener\">GitHub<\/a>.<\/p>\n<hr>\n<h2 id=\"contents\">Contents<\/h2>\n<ol>\n<li><a href=\"#1-quickstart-guide\">Quickstart Guide<\/a><\/li>\n<li><a href=\"#2-project-components\">Project Components<\/a>\n<ul>\n<li><a href=\"#i-project-setup\">i. Project Setup<\/a><\/li>\n<li><a href=\"#ii-core-pipeline-code\">ii. Core Pipeline Code<\/a><\/li>\n<li><a href=\"#iii-spark-toolbox\">iii. Spark Toolbox<\/a><\/li>\n<li><a href=\"#iv-test-framework\">iv. Test Framework<\/a><\/li>\n<li><a href=\"#iv-developer-experience-and-ci-scalafmt-scalafix-github-actions\">v. Developer Experience and CI (Scalafmt, Scalafix, Github Actions)<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#3-file-structure-overview\">File Structure Overview<\/a><\/li>\n<li><a href=\"#4-principles-and-best-practices\">Principles and Best Practices<\/a><\/li>\n<\/ol>\n<h2 id=\"1-quickstart-guide\">1. Quickstart Guide<\/h2>\n<ol>\n<li>\n<p>Create a copy of the template repository using the &ldquo;Use this template&rdquo; button on GitHub.<\/p>\n<img src=\"image.png\" alt=\"Github Use this template\" style=\"max-width:300px; height:auto;\" \/>\n<\/li>\n<li>\n<p>Clone your new repository, and open it on your machine.<\/p>\n<\/li>\n<li>\n<p>Make sure you the required software installed:<\/p>\n<ul>\n<li>Java 17 (recommend <a href=\"https:\/\/www.jenv.be\/\" target=\"_blank\" rel=\"noopener\">jenv<\/a> for managing multiple Java versions)<\/li>\n<li><a href=\"https:\/\/www.scala-lang.org\/download\/\" target=\"_blank\" rel=\"noopener\">Scala 2.13.x<\/a><\/li>\n<li><a href=\"https:\/\/www.scala-sbt.org\/download.html\" target=\"_blank\" rel=\"noopener\">sbt<\/a><\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Run <code>sbt compile<\/code> to download dependencies and compile the project. If it succeeds, everything is working!<\/p>\n<\/li>\n<li>\n<p>Run <code>sbt test<\/code> to run the tests. You should see all tests pass.<\/p>\n<\/li>\n<li>\n<p>When you&rsquo;re ready to build a JAR for deployment, run <code>sbt assembly<\/code>. This will create a fat JAR in <code>target\/scala-2.13\/<\/code> that you can submit to your Spark cluster using <code>spark-submit<\/code>.<\/p>\n<\/li>\n<\/ol>\n<p>If all goes well, you&rsquo;re ready to start writing your own Spark code! Read on for a tour of the project structure and components.<\/p>\n<h2 id=\"2-project-components\">2. Project Components<\/h2>\n<h3 id=\"i-project-setup\">i. Project Setup<\/h3>\n<p>We use <a href=\"https:\/\/www.scala-sbt.org\/\" target=\"_blank\" rel=\"noopener\">sbt<\/a> as our build tool, which is the de facto standard for Scala. The <code>build.sbt<\/code> file specifies our Scala version, dependencies, and other other project settings.<\/p>\n<p>We start with the basic project settings, including the Scala version and organization name.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> organization <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;com.example&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> version <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;0.1.0-SNAPSHOT&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> scalaVersion <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;2.13.12&#34;<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>When managing Spark dependencies, you don&rsquo;t need to bundle in the core Spark libraries with your application. These dependencies are already included in all common Spark deployments. Mark these dependencies as <code>Provided<\/code> to reduce your compiled JAR size and avoid version conflicts. We specify our Iceberg (or other table format) dependencies as <code>Provided<\/code> as well.<\/p>\n<p>However, we do include Spark dependencies in the <code>Compile<\/code> scope for local development and testing.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> libraryDependencies <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-core&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;provided&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-sql&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;provided&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.iceberg&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;iceberg-spark-runtime-3.5_2.13&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;1.4.2&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;provided&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Development dependencies (for local running)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-core&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Compile<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-sql&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Compile<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ CLI Parsing\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;com.github.scopt&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;scopt&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;4.1.0&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>We specify our test dependencies with <code>Test<\/code> scope to prevent them from being included in our production JAR as well. This also includes core Spark libraries, as well as the popular <code>spark-testing-base<\/code> library.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> libraryDependencies <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.scalatest&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;scalatest&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.2.17&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-core&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-sql&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;com.holdenkarau&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-testing-base&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.0_1.4.7&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><details>\n<summary>Expand to view full build.sbt<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> organization <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;com.example&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> version <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;0.1.0-SNAPSHOT&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> scalaVersion <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;2.13.12&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Scalafix settings\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> semanticdbEnabled <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">ThisBuild<\/span> <span style=\"color:#f92672\">\/<\/span> semanticdbVersion <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> scalafixSemanticdb<span style=\"color:#f92672\">.<\/span>revision\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">lazy<\/span> <span style=\"color:#66d9ef\">val<\/span> root <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">(<\/span>project in file<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;.&#34;<\/span><span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">.<\/span>settings<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> name <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">&#34;scala-spark-template-213&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Scala 2.13 settings\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> scalacOptions <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-deprecation&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-feature&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-unchecked&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Xfatal-warnings&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-language:higherKinds&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Wunused:imports,privates,locals,explicits,implicits,params&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Xlint:_&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Ywarn-dead-code&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Ywarn-numeric-widen&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Ywarn-value-discard&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Spark dependencies (provided to reduce jar size)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> libraryDependencies <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-core&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;provided&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-sql&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;provided&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Development dependencies (for local running)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-core&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Compile<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-sql&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Compile<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ CLI argument parsing\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;com.github.scopt&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;scopt&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;4.1.0&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Iceberg support (production)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.iceberg&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;iceberg-spark-runtime-3.5_2.13&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;1.4.2&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;provided&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Test dependencies (include Spark for testing)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.scalatest&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;scalatest&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.2.17&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-core&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;org.apache.spark&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-sql&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.1&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;com.holdenkarau&#34;<\/span> <span style=\"color:#f92672\">%%<\/span> <span style=\"color:#e6db74\">&#34;spark-testing-base&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.5.0_1.4.7&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#a6e22e\">Test<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Assembly settings for fat jar creation\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> assembly <span style=\"color:#f92672\">\/<\/span> assemblyMergeStrategy <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">PathList<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;META-INF&#34;<\/span><span style=\"color:#f92672\">,<\/span> xs <span style=\"color:#66d9ef\">@<\/span> <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">*)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">MergeStrategy<\/span><span style=\"color:#f92672\">.<\/span>discard\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> x <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">MergeStrategy<\/span><span style=\"color:#f92672\">.<\/span>first\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">},<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Assembly jar name\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> assembly <span style=\"color:#f92672\">\/<\/span> assemblyJarName <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>name<span style=\"color:#f92672\">.<\/span>value<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">-<\/span><span style=\"color:#e6db74\">${<\/span>version<span style=\"color:#f92672\">.<\/span>value<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">.jar&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Exclude provided dependencies from assembly\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> assembly <span style=\"color:#f92672\">\/<\/span> assemblyExcludedJars <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> cp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">(<\/span>assembly <span style=\"color:#f92672\">\/<\/span> fullClasspath<span style=\"color:#f92672\">).<\/span>value\n<\/span><\/span><span style=\"display:flex;\"><span> cp filter <span style=\"color:#f92672\">{<\/span> jar <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> jar<span style=\"color:#f92672\">.<\/span>data<span style=\"color:#f92672\">.<\/span>getName<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;spark-&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">},<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Test<\/span> <span style=\"color:#f92672\">\/<\/span> parallelExecution <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">true<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Test<\/span> <span style=\"color:#f92672\">\/<\/span> fork <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">true<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Enable multiple JVMs for better isolation and parallel execution\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Test<\/span> <span style=\"color:#f92672\">\/<\/span> testForkedParallel <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">true<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Limit concurrent tests to avoid resource exhaustion\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Test<\/span> <span style=\"color:#f92672\">\/<\/span> testOptions <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#a6e22e\">Tests<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Argument<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">TestFrameworks<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">ScalaTest<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;-P4&#34;<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Test<\/span> <span style=\"color:#f92672\">\/<\/span> javaOptions <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Xmx3g&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#75715e\">\/\/ Increased memory for parallel execution\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-XX:+UseG1GC&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#75715e\">\/\/ Better GC for concurrent workloads\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-XX:MaxGCPauseMillis=200&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.lang=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.lang.invoke=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.lang.reflect=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.io=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.net=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.nio=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.util=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.util.concurrent=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.util.concurrent.atomic=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.nio.ch=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.nio.cs=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.security.action=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.util.calendar=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.security.jgss\/sun.security.krb5=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/javax.security.auth=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.security.util=ALL-UNNAMED&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Runtime JVM options\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> run <span style=\"color:#f92672\">\/<\/span> javaOptions <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;-Xmx2g&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.lang=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.lang.invoke=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.lang.reflect=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.io=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.net=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.nio=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.util=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.util.concurrent=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/java.util.concurrent.atomic=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.nio.ch=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.nio.cs=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.security.action=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.util.calendar=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.security.jgss\/sun.security.krb5=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/javax.security.auth=ALL-UNNAMED&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;--add-opens=java.base\/sun.security.util=ALL-UNNAMED&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> run <span style=\"color:#f92672\">\/<\/span> fork <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#f92672\">=<\/span> <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><\/details>\n<h3 id=\"ii-core-pipeline-code\">ii. Core Pipeline Code<\/h3>\n<p>The core pipeline code is located in <code>src\/main\/scala\/com\/example\/useractivity\/<\/code>. It is divided into three levels, each with a different testing strategy:<\/p>\n<ul>\n<li><code>App.scala<\/code>: The application entrypoint, which handles argument parsing and job orchestration. This is not tested, as it is a thin wrapper to start the job that depends on the Spark environment.<\/li>\n<li><code>UserActivity.scala<\/code>: The core ETL job logic, which defines the data transformations and business logic. This is covered by end-to-end tests.<\/li>\n<li><code>UserActivityTransformations.scala<\/code>: Pure transformation functions, which are all extensively unit tested.<\/li>\n<\/ul>\n<h3 id=\"iii-spark-toolbox\">iii. Spark Toolbox<\/h3>\n<p>The Spark Toolbox is located in <code>src\/main\/scala\/com\/example\/sparktoolbox\/<\/code>. It provides an abstraction for the most common Spark operations to enable easy testing of core logic. It includes:<\/p>\n<ul>\n<li><code>PlatformProvider.scala<\/code>: A factory object that selects the appropriate platform implementation (production or test) based on the environment.<\/li>\n<li><code>Fetchers.scala<\/code>: Type-safe reading utilities for various data formats (CSV, Parquet, Iceberg, etc.).<\/li>\n<li><code>Writers.scala<\/code>: Production-grade writing utilities with error handling and logging.<\/li>\n<\/ul>\n<p>The <code>PlatformProvider<\/code> automatically selects either <code>production<\/code> or <code>test<\/code> setting based on the whether it&rsquo;s in a ScalaTest thread. Based on this, it provides the right implementation of the Fetchers and Writers: <code>production<\/code> writes to a real table, while <code>test<\/code> saves the dataframe to a map in-memory.<\/p>\n<h3 id=\"iv-test-framework\">iv. Test Framework<\/h3>\n<p>The test framework is located in <code>src\/test\/<\/code>. The two types of tests are unit tests and end-to-end tests<\/p>\n<h4 id=\"unit-tests\">Unit Tests<\/h4>\n<p>Unit tests are located in <code>src\/test\/scala\/com\/example\/useractivity\/UserActivityTransformationsTest.scala<\/code>. They test pure transformation functions using small, hardcoded dataframes. These tests are very quick to set up and run, which makes them ideal for covering all edge cases.<\/p>\n<p>For example, the function <code>filterAndDedupUserEvents<\/code> is reponsible for filtering events outside the date range and deduplicating to the latest by <code>user_id<\/code>. We run this test by passing a small dataset into the function and checking the result:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">TestUserActivityTransformations<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">SparkTestBase<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;filterAndDedupeUserEvents&#34;<\/span> should <span style=\"color:#e6db74\">&#34;filter by date range and deduplicate&#34;<\/span> in <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Arrange\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">import<\/span> spark.implicits._\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Two events for the same user; the later one should be kept after dedupe\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> testEvents <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserEvent<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;click&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1000000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> session_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;session1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> page_url <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;\/page1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> device_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;mobile&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserEvent<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;click&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">2000000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> session_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;session1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> page_url <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;\/page2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> device_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;mobile&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Later event should win\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> inputDs <span style=\"color:#66d9ef\">=<\/span> createTestDataset<span style=\"color:#f92672\">(<\/span>testEvents<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Act\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> result <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">UserActivityTransformations<\/span><span style=\"color:#f92672\">.<\/span>filterAndDedupeUserEvents<span style=\"color:#f92672\">(<\/span>inputDs<span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">20240101<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">20240101<\/span><span style=\"color:#f92672\">)(<\/span>spark<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> resultList <span style=\"color:#66d9ef\">=<\/span> result<span style=\"color:#f92672\">.<\/span>collect<span style=\"color:#f92672\">().<\/span>toList\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Assert: dedupe keeps only the latest event for user1\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> filteredResult <span style=\"color:#66d9ef\">=<\/span> resultList<span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>row_number <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>filteredResult<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> user1Event <span style=\"color:#66d9ef\">=<\/span> filteredResult<span style=\"color:#f92672\">.<\/span>head\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Event<span style=\"color:#f92672\">.<\/span>event_id <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;2&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h4 id=\"end-to-end-tests\">End-to-End Tests<\/h4>\n<p>End-to-end tests are located in <code>src\/test\/scala\/com\/example\/useractivity\/TestE2EUserActivity.scala<\/code>. They test the entire pipeline, including reading input data to writing output data. End-to-end tests catch issues that span across multiple transformations, such as when a column is renamed in one step but not in subsequent steps.<\/p>\n<p>If you have comprehensive end-to-end tests (and unit tests), you can rely primarily on local testing for correctness, avoiding the slow process of running the full job on a cluster.<\/p>\n<p>End-to-end tests are also where our <code>test<\/code> implementations from the Spark Toolbox prove their worth, as testing with reads and writes is seamless.<\/p>\n<p>Pipeline testing with reads and writes is especially valuable for more complex jobs that read and write from multiple tables; our tests can verify that data is managed correctly across all tables.<\/p>\n<details>\n<summary>Expand to view a full end-to-end test<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">package<\/span> com.example.useractivity\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> java.sql.Timestamp\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> com.example.test.SparkTestBase\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> com.example.useractivity.model._\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> sparktoolbox.<span style=\"color:#f92672\">{<\/span><span style=\"color:#a6e22e\">Writers<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Fetchers<\/span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">TestE2EUserActivity<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">SparkTestBase<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;complete ETL pipeline&#34;<\/span> should <span style=\"color:#e6db74\">&#34;process user activity correctly&#34;<\/span> in <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">import<\/span> spark.implicits._\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Setup input data\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> userEvents <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserEvent<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;click&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1000000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> session_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;session1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> page_url <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;\/page1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> device_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;mobile&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserEvent<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;view&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">2000000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> session_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;session1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> page_url <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;\/page2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> device_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;mobile&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserEvent<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;3&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;click&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> event_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1500000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> session_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;session2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> page_url <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;\/page1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> device_type <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;desktop&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> purchases <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">PurchaseTransaction<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> transaction_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;t1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> product_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;prod1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> purchase_amount <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">99.99<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> purchase_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1500000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> payment_method <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;credit&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> currency <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;USD&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> is_refunded <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">false<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">PurchaseTransaction<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> transaction_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;t2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> product_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;prod2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> purchase_amount <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">149.99<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> purchase_timestamp <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1600000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> payment_method <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;paypal&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> currency <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;USD&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> is_refunded <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">false<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> partition_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#ae81ff\">20240101<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> userProfiles <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserProfile<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> username <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;alice&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> age_group <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;25-34&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> country <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;US&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> subscription_tier <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;premium&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> email <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;alice@example.com&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> registration_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">500000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> is_active <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserProfile<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user_id <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;user2&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> username <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;bob&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> age_group <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;35-44&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> country <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;UK&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> subscription_tier <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;basic&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> email <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#e6db74\">&#34;bob@example.com&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> registration_date <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">Timestamp<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">600000<\/span><span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> is_active <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Write input tables to local storage\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Writers<\/span><span style=\"color:#f92672\">.<\/span>writeDatasetToTable<span style=\"color:#f92672\">(<\/span>createTestDataset<span style=\"color:#f92672\">(<\/span>userEvents<span style=\"color:#f92672\">),<\/span> <span style=\"color:#e6db74\">&#34;user_events&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Writers<\/span><span style=\"color:#f92672\">.<\/span>writeDatasetToTable<span style=\"color:#f92672\">(<\/span>createTestDataset<span style=\"color:#f92672\">(<\/span>purchases<span style=\"color:#f92672\">),<\/span> <span style=\"color:#e6db74\">&#34;purchases&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Writers<\/span><span style=\"color:#f92672\">.<\/span>writeDatasetToTable<span style=\"color:#f92672\">(<\/span>createTestDataset<span style=\"color:#f92672\">(<\/span>userProfiles<span style=\"color:#f92672\">),<\/span> <span style=\"color:#e6db74\">&#34;lookup_user&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Execute the job\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">UserActivity<\/span><span style=\"color:#f92672\">.<\/span>run<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> startDate<span style=\"color:#66d9ef\">=<\/span><span style=\"color:#ae81ff\">20240101<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> endDate<span style=\"color:#66d9ef\">=<\/span><span style=\"color:#ae81ff\">20240101<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> userEventsTable<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;user_events&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> purchasesTable<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;purchases&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> outputTable<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;output_table&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Read and verify output\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> result <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Fetchers<\/span><span style=\"color:#f92672\">.<\/span>readTableAsDataset<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">UserActivitySummary<\/span><span style=\"color:#f92672\">](<\/span><span style=\"color:#e6db74\">&#34;output_table&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> resultList <span style=\"color:#66d9ef\">=<\/span> result<span style=\"color:#f92672\">.<\/span>collect<span style=\"color:#f92672\">().<\/span>toList\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Assertions\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>resultList<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> user1Summary <span style=\"color:#66d9ef\">=<\/span> resultList<span style=\"color:#f92672\">.<\/span>find<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>user_id <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;user1&#34;<\/span><span style=\"color:#f92672\">).<\/span>get\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>username <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;alice&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>total_events <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>unique_sessions <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>total_purchases <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>total_purchase_amount <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">99.99<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>most_common_device <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;mobile&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>age_group <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;25-34&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user1Summary<span style=\"color:#f92672\">.<\/span>subscription_tier <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;premium&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> user2Summary <span style=\"color:#66d9ef\">=<\/span> resultList<span style=\"color:#f92672\">.<\/span>find<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>user_id <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;user2&#34;<\/span><span style=\"color:#f92672\">).<\/span>get\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>username <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;bob&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>total_events <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>total_purchases <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>total_purchase_amount <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">149.99<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>most_common_device <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;desktop&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>age_group <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;35-44&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> assert<span style=\"color:#f92672\">(<\/span>user2Summary<span style=\"color:#f92672\">.<\/span>subscription_tier <span style=\"color:#f92672\">==<\/span> <span style=\"color:#e6db74\">&#34;basic&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><\/details>\n<h3 id=\"iv-developer-experience-and-ci-scalafmt-scalafix-github-actions\">iv. Developer Experience and CI (Scalafmt, Scalafix, Github Actions)<\/h3>\n<p>The template uses <a href=\"https:\/\/scalameta.org\/scalafmt\/\" target=\"_blank\" rel=\"noopener\">Scalafmt<\/a> for code formatting and <a href=\"https:\/\/scalacenter.github.io\/scalafix\/\" target=\"_blank\" rel=\"noopener\">Scalafix<\/a> for linting and automatic code fixes, both of which are standard for Scala projects. Configuration files for both tools are included in the project root.<\/p>\n<p>The project also includes a built-in, functional Github Actions workflow (<code>.github\/workflows\/ci.yml<\/code>) that runs on every push and pull request. The workflow checks that the tests are passing, and that the code is linted and formatted correctly.<\/p>\n<h2 id=\"3-file-structure-overview\">3. File Structure Overview<\/h2>\n<p>The project structure is organized as a typical Scala SBT project:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-text\" data-lang=\"text\"><span style=\"display:flex;\"><span>\u251c\u2500\u2500 build.sbt # Build configuration\n<\/span><\/span><span style=\"display:flex;\"><span>\u251c\u2500\u2500 project\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2514\u2500\u2500 plugins.sbt # SBT plugins\n<\/span><\/span><span style=\"display:flex;\"><span>\u251c\u2500\u2500 src\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u251c\u2500\u2500 main\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u251c\u2500\u2500 scala\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u251c\u2500\u2500 com\/example\/useractivity\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 App.scala # Sample application entrypoint\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 UserActivity.scala # Core ETL job logic\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u2514\u2500\u2500 sparktoolbox\/ # Core Spark utilities\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u251c\u2500\u2500 SparkPlatformTrait.scala # Trait for platform abstraction\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u251c\u2500\u2500 SparkPlatform.scala # Production Spark session manager\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u251c\u2500\u2500 PlatformProvider.scala # Factory for platform selection\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u251c\u2500\u2500 Fetchers.scala # Type-safe reading utilities\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u2514\u2500\u2500 Writers.scala # Production-grade writing utilities\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2514\u2500\u2500 resources\/ # Configurations (log4j2.xml, etc.)\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2514\u2500\u2500 test\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u251c\u2500\u2500 scala\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u251c\u2500\u2500 com\/example\/useractivity\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u251c\u2500\u2500 TestE2EUserActivity.scala # End-to-end pipeline tests\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2502 \u2514\u2500\u2500 UserActivityTransformationsTest.scala # Unit tests for transformations\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2514\u2500\u2500 com\/example\/test\/\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2502 \u2514\u2500\u2500 SparkTestBase.scala # Base trait for parallel test execution\n<\/span><\/span><span style=\"display:flex;\"><span>\u2502 \u2514\u2500\u2500 resources\/ # Test resources\n<\/span><\/span><span style=\"display:flex;\"><span>\u251c\u2500\u2500 .scalafmt.conf # Scalafmt configuration\n<\/span><\/span><span style=\"display:flex;\"><span>\u251c\u2500\u2500 .scalafix.conf # Scalafix configuration\n<\/span><\/span><span style=\"display:flex;\"><span>\u2514\u2500\u2500 .gitignore # Git ignore patterns\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"4-principles-and-best-practices\">4. Principles and Best Practices<\/h2>\n<p>As mentioned above, the template is built with several opinionated principles in mind that encourage safer, more robust data engineering:<\/p>\n<ol>\n<li>Testing as a Top-Priority\n<ul>\n<li>Testing is fully setup from the start. All new logic should be covered as a baseline expectation, just like more traditional software engineering projects.<\/li>\n<\/ul>\n<\/li>\n<li>Type Safety\n<ul>\n<li>The project uses Scala traits and case classes to define the schema not only of each table, but also of each intermediate dataframe. This makes the transformations clear to develoeprs, and it can help catch errors during compilation.<\/li>\n<\/ul>\n<\/li>\n<li>Pure Functions and Immutability\n<ul>\n<li>Every transformation function is pure, with no side effects or dependency on external state. This makes the code easier to reason about and test. Dataframes are never modified in place; instead, new dataframes are returned from each transformation.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<p>I highly recommend you keep these principles in mind as you build out your own data engineering projects, whether or not you&rsquo;re using this template. They will pay off in the long run!<\/p>\n<p>Best of luck! If you found this helpful, please consider starring the <a href=\"https:\/\/github.com\/ewoodbury\/spark-template-scala-2.13\" target=\"_blank\" rel=\"noopener\">GitHub repository<\/a>.<\/p>"},{"title":"Rebuilding Apache Spark's StageBuilder from Scratch","link":"https:\/\/ewoodbury.com\/posts\/2025-09-06_sparklet_stagebuilder\/","pubDate":"Sat, 06 Sep 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-09-06_sparklet_stagebuilder\/","description":"<p>I&rsquo;ve been working on rebuilding Apache Spark from scratch in Scala 3 with a project called <strong>Sparklet<\/strong>. My main goal is to learn more about data processing engine internals and distributed systems, but I&rsquo;m also eventually hoping to match or even beat Spark&rsquo;s performance with this rebuild. For more background, see my previous post: <a href=\"https:\/\/ewoodbury.com\/posts\/2025-07-31_sparklet\/\" target=\"_blank\" rel=\"noopener\">Rebuilding Spark with Scala 3<\/a>.<\/p>\n<p>For an engine like Spark, the Stage Builder component is crucial. It translates the logical plan of operations (what the data engineer wrote) into a physical execution plan (how the cluster will actually run the job). Read on for a brief overview of how Sparklet does this.<\/p>\n<h3 id=\"stage-building-concepts\">Stage Building Concepts<\/h3>\n<p>These concepts closely mirror Apache Spark&rsquo;s architecture.<\/p>\n<ul>\n<li>Driver: The main program that coordinates the execution of tasks across the cluster.<\/li>\n<li>Executor: A worker node in the cluster that runs tasks.<\/li>\n<li>Plan: Logical representation of a data processing job. Does not include execution details.<\/li>\n<li>Task: A single unit of work that processes a partition of data (e.g. a Map operation or a Filter operation).<\/li>\n<li>Stage: A group of tasks that can be executed together, without shuffling data.<\/li>\n<li>Stage Boundary: A point where a stage must end, typically due to a shuffle operation.<\/li>\n<li>Shuffle: An operation where data is redistributed across the cluster between executors, to prepare for later stages.<\/li>\n<\/ul>\n<p>The StageBuilder is a single-threaded, local component that runs at the start of a job, which means we fortunately don&rsquo;t need to worry about networking, concurrency, or distribution of logic. The focus here is on correctness and achieving planning optimizations that will be realized later during exeuction.<\/p>\n<h3 id=\"architecture\">Architecture<\/h3>\n<p>Sparklet&rsquo;s StageBuilder follows a simple algorithm:<\/p>\n<ul>\n<li>Recursively go step-by-step through each Task in a Plan:\n<ul>\n<li>If the task is a narrow transformation (e.g. Map, Filter), add it to the current Stage (unless it cannot be chained).<\/li>\n<li>If the task is a wide transformation (e.g. ReduceByKey, GroupByKey), end the current Stage and start a new one.<\/li>\n<li>If the task is a multi-input operation (e.g. Join, Union), ensure that all input Stages are completed, and start a new Stage.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>At the end, the newly-built execution dependency graph is validated for correctness (i.e. acyclic, all stages reachable, no out-of-order stages).<\/p>\n<div class=\"mermaid\">flowchart TD\nA[Plan Tree Input] --> B{Analyze Operation Type}\nB -->|Narrow Operation| C[Accumulate into Current Stage]\nB -->|Wide Operation| D[Create Shuffle Boundary]\nB -->|Multi-Input Operation| E[Create Union Stage]\nC --> F{Can Chain?}\nF -->|Yes| G[Add to Operation Vector]\nF -->|No| H[Create New Stage]\nD --> I[Create Shuffle Stage with WideOp]\nE --> J[Create Stage with Multiple InputSources]\nG --> K[Continue Processing]\nH --> K\nI --> K\nJ --> K\nK --> L{More Operations?}\nL -->|Yes| B\nL -->|No| M[Materialize Operations into Stages]\nM --> N[Build Stage Graph]\nN --> O[Validate Stage Graph]\nclassDef narrow fill:#e1f5fe\nclassDef wide fill:#fff3e0\nclassDef validation fill:#f3e5f5\nclass C,G,H,Q narrow\nclass D,I,R wide\nclass O,T,U,V,W validation\n<\/div>\n<h3 id=\"implementation\">Implementation<\/h3>\n<h4 id=\"key-data-structures\">Key Data Structures<\/h4>\n<ul>\n<li><code>StageInfo<\/code>: Metadata about a single stage, including its ID, the operations it contains, and its input\/output partitioning.<\/li>\n<li><code>InputSource<\/code>: One of the following:\n<ul>\n<li><code>SourceInput<\/code>: Data source from the start of the job<\/li>\n<li><code>ShuffleInput<\/code>: Input from a shuffle operation<\/li>\n<li><code>StageOutput<\/code>: Output of a previous stage<\/li>\n<\/ul>\n<\/li>\n<li><code>StageGraph<\/code>: The final execution plan produced by the StageBuilder. It contains all stages and their dependencies.<\/li>\n<\/ul>\n<h4 id=\"chaining-narrow-operations\">Chaining Narrow Operations<\/h4>\n<p>Of course, we want to keep the final execution graph as simple as possible to reduce overhead, and one of the key ways to do this is keeping the number of stages low.<\/p>\n<p>The StageBuilder attempts to chain together as many narrow operations as possible into a single stage. For example, a sequence of <code>Map<\/code>, <code>Filter<\/code>, and <code>FlatMap<\/code> operations can all be executed in a single stage. We save time by not only skipping any shuffle, but also by eliminating the overhead of additional stages.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">execution<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">def<\/span> materialize<span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;Cannot materialize empty operation vector&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ For single operations, return the operation directly without chaining\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> ops<span style=\"color:#f92672\">.<\/span>headOption <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>op<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> createStageFromOp<span style=\"color:#f92672\">(<\/span>op<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">None<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Single operation vector unexpectedly empty after length check. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;This indicates a concurrent modification or internal error.&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span> <span style=\"color:#66d9ef\">else<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ For multiple operations, build the chain efficiently\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> ops<span style=\"color:#f92672\">.<\/span>headOption <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>firstOp<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ subsequent ops are chained via fold onto the stage from the first op:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> ops<span style=\"color:#f92672\">.<\/span>drop<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">).<\/span>foldLeft<span style=\"color:#f92672\">(<\/span>createStageFromOp<span style=\"color:#f92672\">(<\/span>firstOp<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span> <span style=\"color:#f92672\">(<\/span>stage<span style=\"color:#f92672\">,<\/span> op<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> chainOperationUnsafe<span style=\"color:#f92672\">(<\/span>stage<span style=\"color:#f92672\">,<\/span> op<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">None<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Multiple operation vector unexpectedly empty after validation. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Original vector length: <\/span><span style=\"color:#e6db74\">${<\/span>ops<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;This indicates a concurrent modification or internal error.&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p><a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/3dec10e5bb9d58768944416160b78bdc01c03832\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/StageBuilder.scala#L465-L493\" target=\"_blank\" rel=\"noopener\">Link<\/a><\/p>\n<h4 id=\"shuffle-detection\">Shuffle Detection<\/h4>\n<p>Shuffle detection at its core is straightforward. If the next operation in the plan is a wide operation, end the current stage and start a new one.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">def<\/span> isDirectlyWide<span style=\"color:#f92672\">(<\/span>plan<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Boolean<\/span> <span style=\"color:#f92672\">=<\/span> plan <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.GroupByKeyOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.ReduceByKeyOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.SortByOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.JoinOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.CoGroupOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.RepartitionOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.CoalesceOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Plan.PartitionByOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">true<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">false<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p><a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/3dec10e5bb9d58768944416160b78bdc01c03832\/modules\/sparklet-core\/src\/main\/scala\/com\/ewoodbury\/sparklet\/core\/PlanWide.scala#L61\" target=\"_blank\" rel=\"noopener\">Link<\/a><\/p>\n<h4 id=\"shuffle-bypass-optimization\">Shuffle Bypass Optimization<\/h4>\n<p>Fortunately for us, not every wide operation needs a shuffle! This is most commonly the case when a Plan has consecutive wide transformations over the same key, such as a <code>GroupByKey<\/code> followed by a <code>ReduceByKey<\/code>. t can also occur when after a series of narrow transformations if the data is still partitioned by the required key.<\/p>\n<p>If you&rsquo;ve built massive Spark jobs before, you&rsquo;re well aware of this fact, since you&rsquo;ve probably spent many hours ensuring consistent paritioning across your data and\/or pipeline to prevent even a single unnecessary shuffle.<\/p>\n<p>Here&rsquo;s the Sparklet code that determines if a shuffle can be bypassed:<\/p>\n<details>\n<summary>Click to expand<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/**\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * Optimization hook to determine if a shuffle operation can be bypassed based on upstream\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * partitioning metadata. This generalizes the current groupByKey\/reduceByKey shortcut.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">execution<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">def<\/span> canBypassShuffle<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> plan<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> upstreamPartitioning<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Option<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">com.ewoodbury.sparklet.execution.StageBuilder.Partitioning<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> conf<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">com.ewoodbury.sparklet.core.SparkletConf<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Boolean<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> plan <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> gbk<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan.GroupByKeyOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Can bypass if already partitioned by key with correct partition count\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> upstreamPartitioning<span style=\"color:#f92672\">.<\/span>exists<span style=\"color:#f92672\">(<\/span>p <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> p<span style=\"color:#f92672\">.<\/span>byKey <span style=\"color:#f92672\">&amp;&amp;<\/span> p<span style=\"color:#f92672\">.<\/span>numPartitions <span style=\"color:#f92672\">==<\/span> conf<span style=\"color:#f92672\">.<\/span>defaultShufflePartitions<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> rbk<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan.ReduceByKeyOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Can bypass if already partitioned by key with correct partition count\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> upstreamPartitioning<span style=\"color:#f92672\">.<\/span>exists<span style=\"color:#f92672\">(<\/span>p <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> p<span style=\"color:#f92672\">.<\/span>byKey <span style=\"color:#f92672\">&amp;&amp;<\/span> p<span style=\"color:#f92672\">.<\/span>numPartitions <span style=\"color:#f92672\">==<\/span> conf<span style=\"color:#f92672\">.<\/span>defaultShufflePartitions<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> pby<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan.PartitionByOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/* Can bypass PartitionBy if upstream is already partitioned by key with correct partition\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * count *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ This enables chaining: partitionBy -&gt; groupByKey to be optimized to a single stage\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> upstreamPartitioning<span style=\"color:#f92672\">.<\/span>exists<span style=\"color:#f92672\">(<\/span>p <span style=\"color:#66d9ef\">=&gt;<\/span> p<span style=\"color:#f92672\">.<\/span>byKey <span style=\"color:#f92672\">&amp;&amp;<\/span> p<span style=\"color:#f92672\">.<\/span>numPartitions <span style=\"color:#f92672\">==<\/span> pby<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> rep<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan.RepartitionOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Can bypass if already has the desired partitioning\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> upstreamPartitioning<span style=\"color:#f92672\">.<\/span>exists<span style=\"color:#f92672\">(<\/span>p <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#f92672\">!<\/span>p<span style=\"color:#f92672\">.<\/span>byKey <span style=\"color:#f92672\">&amp;&amp;<\/span> p<span style=\"color:#f92672\">.<\/span>numPartitions <span style=\"color:#f92672\">==<\/span> rep<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> coal<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan.CoalesceOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/* Coalesce always requires shuffle - cannot bypass even if partition count is already\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * correct *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ because it may need to redistribute data across partitions\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">false<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Other wide operations cannot bypass shuffle\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">false<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p><a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/3dec10e5bb9d58768944416160b78bdc01c03832\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/Operation.scala#L138-L176\" target=\"_blank\" rel=\"noopener\">Link<\/a><\/p>\n<\/details>\n<br\/>\n<p>Once the StageBuilder knows a shuffle can be bypassed, it implements the optimization by simply not creating a new stage. The wide transformation is added to the current stage instead.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ within StageBuilder.buildStagesFromPlan:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> buildStagesFromPlan<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span><span style=\"color:#f92672\">](<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> ctx<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">BuildContext<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> plan<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> builderMap<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">mutable.Map<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span>, <span style=\"color:#66d9ef\">StageDraft<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> dependencies<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">mutable.Map<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span>, <span style=\"color:#66d9ef\">mutable.Set<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">]],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#66d9ef\">Option<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Plan<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]])<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">.<\/span>canBypassShuffle<span style=\"color:#f92672\">(<\/span>groupByKey<span style=\"color:#f92672\">,<\/span> src<span style=\"color:#f92672\">.<\/span>outputPartitioning<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">SparkletConf<\/span><span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Add local groupByKey operation to existing stage since shuffle can be bypassed\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> resultId <span style=\"color:#66d9ef\">=<\/span> appendOperation<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> ctx<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> sourceStageId<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">GroupByKeyLocalOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]().<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> builderMap<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> dependencies<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">(<\/span>resultId<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>groupByKey<span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p><a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/3dec10e5bb9d58768944416160b78bdc01c03832\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/StageBuilder.scala#L847-L856\" target=\"_blank\" rel=\"noopener\">Link<\/a><\/p>\n<h4 id=\"creating-a-shuffle-boundary\">Creating a Shuffle Boundary<\/h4>\n<p>If Sparklet has determined that a shuffle is unavoidable, it creates a shuffle boundary when processing that operation as follows:<\/p>\n<ul>\n<li>Function calls <code>createShuffleStageUnified<\/code>, which:\n<ul>\n<li>Validates that all input stages are complete<\/li>\n<li>Creates a new <code>StageDraft<\/code> object corresponding to the operation type (e.g. <code>GroupByKeyWideOp<\/code>)<\/li>\n<li>Sets up <code>ShuffleInput<\/code> sources for the new stage, pointing to the output of the input stages<\/li>\n<li>Updates the dependency graph to reflect the new stage and its inputs<\/li>\n<li>Returns the new stage ID<\/li>\n<\/ul>\n<\/li>\n<li>Main function then appends the shuffle stage ID, then the new operation to the current plan as a new stage.<\/li>\n<\/ul>\n<details>\n<summary>Click to expand code<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Within StageBuilder.buildStagesFromPlan:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Wide transformations (create shuffle boundaries)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> groupByKey<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Plan.GroupByKeyOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> <span style=\"color:#f92672\">(<\/span>sourceStageId<span style=\"color:#f92672\">,<\/span> <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> buildStagesFromPlan<span style=\"color:#f92672\">(<\/span>ctx<span style=\"color:#f92672\">,<\/span> groupByKey<span style=\"color:#f92672\">.<\/span>source<span style=\"color:#f92672\">,<\/span> builderMap<span style=\"color:#f92672\">,<\/span> dependencies<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> src <span style=\"color:#66d9ef\">=<\/span> builderMap<span style=\"color:#f92672\">(<\/span>sourceStageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> defaultN <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">SparkletConf<\/span><span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">.<\/span>defaultShufflePartitions\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">.<\/span>canBypassShuffle<span style=\"color:#f92672\">(<\/span>groupByKey<span style=\"color:#f92672\">,<\/span> src<span style=\"color:#f92672\">.<\/span>outputPartitioning<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">SparkletConf<\/span><span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span> <span style=\"color:#75715e\">\/\/ Shuffle is bypassed in this case.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span> <span style=\"color:#66d9ef\">else<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> shuffleId <span style=\"color:#66d9ef\">=<\/span> createShuffleStageUnified<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> ctx<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>sourceStageId<span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">GroupByKeyWideOp<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">SimpleWideOpMeta<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> kind <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">GroupByKey<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> numPartitions <span style=\"color:#66d9ef\">=<\/span> defaultN<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> builderMap<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> dependencies<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>groupByKey<span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">(<\/span>shuffleId<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>groupByKey<span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Within StageBuilder.createShuffleStageUnified:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Create shuffle input sources based on operation type\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> shuffleInputSources <span style=\"color:#66d9ef\">=<\/span> meta<span style=\"color:#f92672\">.<\/span>kind <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Join<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">CoGroup<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Skip multi-stage operations for brevity here\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Single-input operations\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>meta<span style=\"color:#f92672\">.<\/span>kind<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> requires exactly 1 upstream stage&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> upstreamIds<span style=\"color:#f92672\">.<\/span>headOption <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Create ShuffleInput from the latest upstream stage:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>upstreamId<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstreamId<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">None<\/span><span style=\"color:#f92672\">,<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">None<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>meta<span style=\"color:#f92672\">.<\/span>kind<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> operation missing required upstream stage&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Add dependencies for all upstream stages\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> upstreamIds<span style=\"color:#f92672\">.<\/span>foreach <span style=\"color:#f92672\">{<\/span> upstreamId <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> addDependency<span style=\"color:#f92672\">(<\/span>dependencies<span style=\"color:#f92672\">,<\/span> shuffleStageId<span style=\"color:#f92672\">,<\/span> upstreamId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Return the new Shuffle stage ID:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> shuffleStageId\n<\/span><\/span><\/code><\/pre><\/div><p><a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/3dec10e5bb9d58768944416160b78bdc01c03832\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/StageBuilder.scala#L841\" target=\"_blank\" rel=\"noopener\">Link<\/a><\/p>\n<\/details>\n<h3 id=\"validation\">Validation<\/h3>\n<p>The final main step is the validation of the generated graph. We run the following checks:<\/p>\n<ul>\n<li>Acyclic: Ensure there are no cycles in the stage dependency graph.<\/li>\n<li>Reachability: Ensure all stages are reachable from the initial data sources.<\/li>\n<li>Order: Ensure that no stage depends on a future stage (i.e. all dependencies are valid).<\/li>\n<li>Shuffle invariants: shuffle stages must not have empty shuffle metadata, shuffle stages must have empty transformation metadata, multi-input stages must have multiple input sources.<\/li>\n<li>Partitioning invariants: <code>byKey<\/code> partition must only exist on keyed operations (groupByKey, reduceByKey, join, etc.)<\/li>\n<\/ul>\n<details>\n<summary>Click to expand code<\/summary>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/**\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * Validates that the stage graph is acyclic using depth-first search.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> validateAcyclicity<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">StageGraph<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> visiting <span style=\"color:#66d9ef\">=<\/span> mutable<span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Set<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">]()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> visited <span style=\"color:#66d9ef\">=<\/span> mutable<span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Set<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">]()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">def<\/span> dfsVisit<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>visiting<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> cyclePath <span style=\"color:#66d9ef\">=<\/span> visiting<span style=\"color:#f92672\">.<\/span>toSeq <span style=\"color:#66d9ef\">:<\/span><span style=\"color:#66d9ef\">+<\/span> <span style=\"color:#66d9ef\">stageId<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Cycle detected in stage graph involving stage <\/span><span style=\"color:#e6db74\">$stageId<\/span><span style=\"color:#e6db74\">. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Cycle path: <\/span><span style=\"color:#e6db74\">${<\/span>cyclePath<span style=\"color:#f92672\">.<\/span>mkString<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34; -&gt; &#34;<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;This indicates a circular dependency that would prevent execution.&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>visited<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> visiting<span style=\"color:#f92672\">.<\/span>add<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> graph<span style=\"color:#f92672\">.<\/span>dependencies<span style=\"color:#f92672\">.<\/span>getOrElse<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Set<\/span><span style=\"color:#f92672\">.<\/span>empty<span style=\"color:#f92672\">).<\/span>foreach<span style=\"color:#f92672\">(<\/span>dfsVisit<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> visiting<span style=\"color:#f92672\">.<\/span>remove<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> visited<span style=\"color:#f92672\">.<\/span>add<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> graph<span style=\"color:#f92672\">.<\/span>stages<span style=\"color:#f92672\">.<\/span>keys<span style=\"color:#f92672\">.<\/span>foreach <span style=\"color:#f92672\">{<\/span> stageId <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(!<\/span>visited<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> dfsVisit<span style=\"color:#f92672\">(<\/span>stageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/**\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * Validates that all stages are reachable from the final stage via reverse traversal.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> validateReachability<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">StageGraph<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> reachable <span style=\"color:#66d9ef\">=<\/span> mutable<span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Set<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">]()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> toVisit <span style=\"color:#66d9ef\">=<\/span> mutable<span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Queue<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">StageId<\/span><span style=\"color:#f92672\">]()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Start from finalStageId and traverse backwards through dependencies\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> toVisit<span style=\"color:#f92672\">.<\/span>enqueue<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#f92672\">.<\/span>finalStageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> reachable<span style=\"color:#f92672\">.<\/span>add<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#f92672\">.<\/span>finalStageId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">while<\/span> <span style=\"color:#f92672\">(<\/span>toVisit<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> current <span style=\"color:#66d9ef\">=<\/span> toVisit<span style=\"color:#f92672\">.<\/span>dequeue<span style=\"color:#f92672\">()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> graph<span style=\"color:#f92672\">.<\/span>dependencies<span style=\"color:#f92672\">.<\/span>getOrElse<span style=\"color:#f92672\">(<\/span>current<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Set<\/span><span style=\"color:#f92672\">.<\/span>empty<span style=\"color:#f92672\">).<\/span>foreach <span style=\"color:#f92672\">{<\/span> depId <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(!<\/span>reachable<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span>depId<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> reachable<span style=\"color:#f92672\">.<\/span>add<span style=\"color:#f92672\">(<\/span>depId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> toVisit<span style=\"color:#f92672\">.<\/span>enqueue<span style=\"color:#f92672\">(<\/span>depId<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check for orphaned stages\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> allStageIds <span style=\"color:#66d9ef\">=<\/span> graph<span style=\"color:#f92672\">.<\/span>stages<span style=\"color:#f92672\">.<\/span>keySet\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> orphaned <span style=\"color:#66d9ef\">=<\/span> allStageIds <span style=\"color:#f92672\">--<\/span> reachable\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>orphaned<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> reachableStages <span style=\"color:#66d9ef\">=<\/span> reachable<span style=\"color:#f92672\">.<\/span>toSeq<span style=\"color:#f92672\">.<\/span>sorted<span style=\"color:#f92672\">.<\/span>mkString<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;, &#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Orphaned stages not reachable from finalStageId <\/span><span style=\"color:#e6db74\">${<\/span>graph<span style=\"color:#f92672\">.<\/span>finalStageId<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">: [<\/span><span style=\"color:#e6db74\">${<\/span>orphaned<span style=\"color:#f92672\">.<\/span>toSeq<span style=\"color:#f92672\">.<\/span>sorted<span style=\"color:#f92672\">.<\/span>mkString<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;, &#34;<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">]. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Reachable stages: [<\/span><span style=\"color:#e6db74\">$reachableStages<\/span><span style=\"color:#e6db74\">]. &#34;<\/span> <span style=\"color:#f92672\">+<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">&#34;Check for disconnected stage subgraphs or missing dependencies.&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/**\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * Validates that stage IDs form a monotonic sequence (strictly increasing from 0).\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> validateStageIdMonotonicity<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">StageGraph<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> stageIds <span style=\"color:#66d9ef\">=<\/span> graph<span style=\"color:#f92672\">.<\/span>stages<span style=\"color:#f92672\">.<\/span>keys<span style=\"color:#f92672\">.<\/span>toSeq<span style=\"color:#f92672\">.<\/span>sorted\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>stageIds<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check starts from 0\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> stageIds<span style=\"color:#f92672\">.<\/span>headOption <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>firstId<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">if<\/span> firstId<span style=\"color:#f92672\">.<\/span>toInt <span style=\"color:#f92672\">!=<\/span> <span style=\"color:#ae81ff\">0<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Stage IDs should start from 0, but found minimum ID: <\/span><span style=\"color:#e6db74\">${<\/span>firstId<span style=\"color:#f92672\">.<\/span>toInt<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">None<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ This case shouldn&#39;t happen since we check nonEmpty above, but handle defensively\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;Stage ID sequence is unexpectedly empty&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#75715e\">\/\/ firstId.toInt == 0, which is correct\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Check for gaps (warn only to future-proof ID reuse scenarios)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> expectedSequence <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">0<\/span> until stageIds<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#f92672\">).<\/span>map<span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">StageId<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> actualSet <span style=\"color:#66d9ef\">=<\/span> stageIds<span style=\"color:#f92672\">.<\/span>toSet\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> missing <span style=\"color:#66d9ef\">=<\/span> expectedSequence<span style=\"color:#f92672\">.<\/span>filterNot<span style=\"color:#f92672\">(<\/span>actualSet<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>missing<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Use println instead of logging to avoid dependencies\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> println<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Warning: Stage ID sequence has gaps. Missing IDs: <\/span><span style=\"color:#e6db74\">${<\/span>missing<span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>toInt<span style=\"color:#f92672\">).<\/span>mkString<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;, &#34;<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/**\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * Validates shuffle stage specific invariants.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> validateShuffleStages<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">StageGraph<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> graph<span style=\"color:#f92672\">.<\/span>stages<span style=\"color:#f92672\">.<\/span>values<span style=\"color:#f92672\">.<\/span>foreach <span style=\"color:#f92672\">{<\/span> stageInfo <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>isShuffleStage<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Shuffle stages should have shuffle operation metadata\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>shuffleOperation<span style=\"color:#f92672\">.<\/span>isEmpty<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Shuffle stage <\/span><span style=\"color:#e6db74\">${<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>id<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> has no shuffle operation metadata&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Shuffle stages should have empty ops vector (they don&#39;t execute operations)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> stageInfo<span style=\"color:#f92672\">.<\/span>stage <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Stage.ChainedStage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ ChainedStage should not be used for shuffle stages\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Shuffle stage <\/span><span style=\"color:#e6db74\">${<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>id<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> incorrectly uses ChainedStage&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#75715e\">\/\/ Other stage types are acceptable for shuffle stages\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Multi-input shuffle stages should have proper side markers\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>inputSources<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> shuffleInputs <span style=\"color:#66d9ef\">=<\/span> stageInfo<span style=\"color:#f92672\">.<\/span>inputSources<span style=\"color:#f92672\">.<\/span>collect <span style=\"color:#f92672\">{<\/span> <span style=\"color:#66d9ef\">case<\/span> si<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">ShuffleInput<\/span> <span style=\"color:#f92672\">=&gt;<\/span> si <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>shuffleInputs<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">!=<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Multi-input shuffle stage <\/span><span style=\"color:#e6db74\">${<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>id<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> should have exactly 2 ShuffleInputs, found <\/span><span style=\"color:#e6db74\">${<\/span>shuffleInputs<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>shuffleInputs<span style=\"color:#f92672\">.<\/span>exists<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>side<span style=\"color:#f92672\">.<\/span>isEmpty<span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Multi-input shuffle stage <\/span><span style=\"color:#e6db74\">${<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>id<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> has ShuffleInputs without side markers&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> sides <span style=\"color:#66d9ef\">=<\/span> shuffleInputs<span style=\"color:#f92672\">.<\/span>flatMap<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>side<span style=\"color:#f92672\">).<\/span>toSet\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>sides<span style=\"color:#f92672\">.<\/span>size <span style=\"color:#f92672\">!=<\/span> <span style=\"color:#ae81ff\">2<\/span> <span style=\"color:#f92672\">||<\/span> <span style=\"color:#f92672\">!<\/span>sides<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">Side<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Left<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">||<\/span> <span style=\"color:#f92672\">!<\/span>sides<span style=\"color:#f92672\">.<\/span>contains<span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">Side<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Right<\/span><span style=\"color:#f92672\">))<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Multi-input shuffle stage <\/span><span style=\"color:#e6db74\">${<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>id<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> has invalid side markers: expected {Left, Right}, found <\/span><span style=\"color:#e6db74\">$sides<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Validate numPartitions consistency across inputs\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> numPartitionsList <span style=\"color:#66d9ef\">=<\/span> shuffleInputs<span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">).<\/span>distinct\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>numPartitionsList<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">&gt;<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">IllegalStateException<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#e6db74\">s&#34;Multi-input shuffle stage <\/span><span style=\"color:#e6db74\">${<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>id<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> has mismatched numPartitions across inputs: <\/span><span style=\"color:#e6db74\">${<\/span>numPartitionsList<span style=\"color:#f92672\">.<\/span>mkString<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;, &#34;<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/**\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * Validates partitioning invariants - byKey should only be true for operations that guarantee\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * key grouping.\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> validatePartitioningInvariants<span style=\"color:#f92672\">(<\/span>graph<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">StageGraph<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Unit<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/* Operations that guarantee key grouping - simplified approach since classOf with generics is\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * problematic *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ In practice, this would be implemented with a more sophisticated operation analysis system\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> graph<span style=\"color:#f92672\">.<\/span>stages<span style=\"color:#f92672\">.<\/span>values<span style=\"color:#f92672\">.<\/span>foreach <span style=\"color:#f92672\">{<\/span> stageInfo <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> stageInfo<span style=\"color:#f92672\">.<\/span>outputPartitioning<span style=\"color:#f92672\">.<\/span>foreach <span style=\"color:#f92672\">{<\/span> partitioning <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>partitioning<span style=\"color:#f92672\">.<\/span>byKey<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ For byKey=true, verify stage has key-grouping operations or is a shuffle stage\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(!<\/span>stageInfo<span style=\"color:#f92672\">.<\/span>isShuffleStage<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> stageInfo<span style=\"color:#f92672\">.<\/span>stage <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_:<\/span> <span style=\"color:#66d9ef\">Stage.ChainedStage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ For chained stages, check if any operation guarantees key grouping\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/* Note: This is a simplified check - in practice we&#39;d need to analyze the operation\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * chain *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ For now, we&#39;ll be permissive and allow byKey=true if it&#39;s explicitly set\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/* This validation can be strengthened in the future with more detailed operation\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\"> * analysis *\/<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ For single operation stages, we could check the specific operation type\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ But since we don&#39;t have direct access to the operation, we&#39;ll be permissive here\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p><a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/3dec10e5bb9d58768944416160b78bdc01c03832\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/StageBuilder.scala#L261\" target=\"_blank\" rel=\"noopener\">Link<\/a><\/p>\n<\/details>\n<br\/>\nIf all of these checks pass, the StageBuilder was successful! It returns a final `StageGraph` object that is passed to the Sparklet engine for execution.\n<hr>\n<p>Thanks for reading! I plan to do more deep dives into the Sparklet internals over time. Feel free to follow along on the <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\" target=\"_blank\" rel=\"noopener\">GitHub repo<\/a> for updates in real-time. Until next time!<\/p>"},{"title":"Safer Scala with WartRemover Part 3: Null and Option.get","link":"https:\/\/ewoodbury.com\/posts\/2025-09-01_wartremover_null\/","pubDate":"Mon, 01 Sep 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-09-01_wartremover_null\/","description":"<p>This is the third post in a series about using WartRemover to improve Scala code quality, and more broadly about language safety.<\/p>\n<h2 id=\"nulls\">Nulls<\/h2>\n<p>Nulls (<code>None<\/code> for Pythonistas) are a common part of most programming languages, providing the escape hatch for unknown values such as API responses, or data inputs which can be missing.<\/p>\n<p>However, type-safety requires an expression to preserve its type <code>T<\/code> through any allowed transformation, which improves program reliability by eliminating unexpected states. Nulls violate this constraint by the simple fact that nulls may cause a program which normally succeeds in returning a value with type <code>T<\/code>, to instead fail with a null-pointer exception. Practically, nulls are often defined far from where they are actually used, have little-to-no visible signature, and can quickly propagate everywhere causing every function to guard against them.<\/p>\n<p>Hence the legendary Tony Hoare (from <a href=\"https:\/\/en.wikipedia.org\/wiki\/Hoare_logic\" target=\"_blank\" rel=\"noopener\">Hoare logic<\/a>) called his invention of the null reference his <a href=\"https:\/\/www.infoq.com\/presentations\/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare\/\" target=\"_blank\" rel=\"noopener\">&ldquo;billion-dollar mistake&rdquo;<\/a>.<\/p>\n<h2 id=\"nulls-in-scala\">Nulls In Scala<\/h2>\n<p>Much work has been put into handling nulls better across different programming languages. Scala in particular allows regular <code>Null<\/code> values, but it is discouraged. Scala 3 also added an opt-in feature for Explicit Nulls, which improves things by making reference types non-nullable by default, but it still leaves room for mistakes.<\/p>\n<p>Common Scala guidance is to never use nulls within your own code, and to use <code>Option[T]<\/code> types instead. For Java interoperability, convert all nulls to <code>Option(javaApi.getObject())<\/code> at the boundary.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> findUser<span style=\"color:#f92672\">(<\/span>id<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Option<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>findUser<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>user<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> user<span style=\"color:#f92672\">.<\/span>name\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">None<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">&#34;unknown&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ or:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>findUser<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">).<\/span>map<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>name<span style=\"color:#f92672\">).<\/span>get\n<\/span><\/span><\/code><\/pre><\/div><p>The beauty of this approach, and of banning all nulls via WartRemover, is that nulls can no longer exist as umbrella types at all. We can rule out all bugs due to a normal-looking type hiding a null value. All missing values are now expressed in the first-class types, which means the Scala compiler now forces you to handle them.<\/p>\n<p>For cases where the missing value should cause the program to fail visibly, use <code>Either[E, T]<\/code> instead:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> findUser<span style=\"color:#f92672\">(<\/span>id<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Int<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Either<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span>findUser<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Right<\/span><span style=\"color:#f92672\">(<\/span>user<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> user<span style=\"color:#f92672\">.<\/span>name\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Left<\/span><span style=\"color:#f92672\">(<\/span>err<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">s&#34;failed: <\/span><span style=\"color:#e6db74\">$err<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ or using fold:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>findUser<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">).<\/span>fold<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> err <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">s&#34;failed: <\/span><span style=\"color:#e6db74\">$err<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user <span style=\"color:#66d9ef\">=&gt;<\/span> user<span style=\"color:#f92672\">.<\/span>name\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>In a similar vein, we need to ban raw <code>.get<\/code> calls for the same reason, that it could be used to unwrap an <code>Option<\/code> variable which could either succeed or crash. Instead we use <code>getOrElse()<\/code>, so we&rsquo;re forced to provide a default value for the missing case.<\/p>\n<p>If you have a sharp eye, you noticed the unsafe <code>.get<\/code> call above; the fixed version is:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span>findUser<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">).<\/span>map<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">.<\/span>name<span style=\"color:#f92672\">).<\/span>getOrElse<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;unknown&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>If the missing value is a failure using <code>Either<\/code>, the alternative here for <code>.get<\/code> would be <code>toRight<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span>findUser<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">).<\/span>toRight<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;User not found&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"full-example\">Full Example<\/h2>\n<p>We show the value of banning nulls in a slightly larger program here, with a naive approach:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> welcomeEmail<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> userId<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> users<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Map<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> prefs<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Map<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> user <span style=\"color:#66d9ef\">=<\/span> users<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">(<\/span>userId<span style=\"color:#f92672\">).<\/span>getOrElse<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">null<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>user <span style=\"color:#f92672\">==<\/span> <span style=\"color:#66d9ef\">null<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">return<\/span> <span style=\"color:#66d9ef\">null<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> email <span style=\"color:#66d9ef\">=<\/span> user<span style=\"color:#f92672\">.<\/span>email <span style=\"color:#75715e\">\/\/ Java-nullable String\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>email <span style=\"color:#f92672\">==<\/span> <span style=\"color:#66d9ef\">null<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">RuntimeException<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">$userId<\/span><span style=\"color:#e6db74\"> has no email&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> name <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Option<\/span><span style=\"color:#f92672\">(<\/span>user<span style=\"color:#f92672\">.<\/span>name<span style=\"color:#f92672\">).<\/span>getOrElse<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;there&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> locale <span style=\"color:#66d9ef\">=<\/span> prefs<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;locale&#34;<\/span><span style=\"color:#f92672\">).<\/span>getOrElse<span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">null<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> subject <span style=\"color:#66d9ef\">=<\/span> prefs<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;subject&#34;<\/span><span style=\"color:#f92672\">).<\/span>get\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>locale <span style=\"color:#f92672\">==<\/span> <span style=\"color:#66d9ef\">null<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">$subject\\nHi<\/span><span style=\"color:#e6db74\"> <\/span><span style=\"color:#e6db74\">$name<\/span><span style=\"color:#e6db74\"> &lt;<\/span><span style=\"color:#e6db74\">$email<\/span><span style=\"color:#e6db74\">&gt;&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">else<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">$subject\\nHi<\/span><span style=\"color:#e6db74\"> <\/span><span style=\"color:#e6db74\">$name<\/span><span style=\"color:#e6db74\"> &lt;<\/span><span style=\"color:#e6db74\">$email<\/span><span style=\"color:#e6db74\">&gt; (<\/span><span style=\"color:#e6db74\">$locale<\/span><span style=\"color:#e6db74\">)&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Not only are there ugly null guards sprinkled throughout the code; the entire function return type lies since it can return null in its entirety. It&rsquo;s silly this compiles at all!<\/p>\n<p>Here&rsquo;s the cleaned up snippet conforming to the WartRemover rules:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">WelcomeEmail<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> subject<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> name<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> email<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> locale<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Option<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> welcomeEmail<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> userId<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> users<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Map<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">User<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> prefs<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Map<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Either<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">WelcomeEmail<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> user <span style=\"color:#66d9ef\">&lt;-<\/span> users<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">(<\/span>userId<span style=\"color:#f92672\">).<\/span>toRight<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">s&#34;unknown user <\/span><span style=\"color:#e6db74\">$userId<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> email <span style=\"color:#66d9ef\">&lt;-<\/span> <span style=\"color:#a6e22e\">Option<\/span><span style=\"color:#f92672\">(<\/span>user<span style=\"color:#f92672\">.<\/span>email<span style=\"color:#f92672\">).<\/span>toRight<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">$userId<\/span><span style=\"color:#e6db74\"> has no email&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> subject <span style=\"color:#66d9ef\">&lt;-<\/span> prefs<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;subject&#34;<\/span><span style=\"color:#f92672\">).<\/span>toRight<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;missing subject&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span> <span style=\"color:#66d9ef\">yield<\/span> <span style=\"color:#a6e22e\">WelcomeEmail<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> subject <span style=\"color:#66d9ef\">=<\/span> subject<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> name <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">Option<\/span><span style=\"color:#f92672\">(<\/span>user<span style=\"color:#f92672\">.<\/span>name<span style=\"color:#f92672\">).<\/span>getOrElse<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;there&#34;<\/span><span style=\"color:#f92672\">),<\/span> <span style=\"color:#75715e\">\/\/ real default\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> email <span style=\"color:#66d9ef\">=<\/span> email<span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> locale <span style=\"color:#66d9ef\">=<\/span> prefs<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;locale&#34;<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ stays optional\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h2 id=\"wartremover\">WartRemover<\/h2>\n<p>For completeness, to set these rules simply set in your build.sbt:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span>wartremoverErrors <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Wart<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Null<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Wart<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">OptionPartial<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Scala still needs to deal with nulls from libraries and Java interoperability, so this isn&rsquo;t quite as complete as Haskell or Rust where nulls are fully nonexistent. Still, WartRemover gets Scala as close as you can get to these strict languages, within the JVM ecosystem.<\/p>"},{"title":"Safer Scala with WartRemover Part 2: A Basic Guide to Removing Any","link":"https:\/\/ewoodbury.com\/posts\/2025-08-31_wartremover_any\/","pubDate":"Sun, 31 Aug 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-08-31_wartremover_any\/","description":"<p>This is the second in a series of posts about improving Scala code quality using the WartRemover static analysis tool, and more broadly about language restrictions to improve program correctness.<\/p>\n<p>The WartRemover Any rule is straightfoward; it bans the use of the <code>Any<\/code> type, including <code>AnyVal<\/code> and <code>AnyRef<\/code>.<\/p>\n<p>See <a href=\"https:\/\/ewoodbury.com\/posts\/2025-08-25_head_seq_apply_wartremover\/\" target=\"_blank\" rel=\"noopener\">Part 1<\/a> for how to set up WartRemover. Once you&rsquo;re set up, simply add the following to your <code>build.sbt<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span>wartremoverErrors <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#a6e22e\">WartRemover<\/span><span style=\"color:#f92672\">.<\/span>warts<span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Any<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>I&rsquo;ll use this post to show a couple interesting cases where I&rsquo;ve used Scala&rsquo;s type system to model complex programs correctly, and thus either remove <code>Any<\/code> or at least isolate its usage to a small surface area.<\/p>\n<hr>\n<h3 id=\"1-basic-example\">1. Basic Example<\/h3>\n<p>Let&rsquo;s write a simple <a href=\"https:\/\/docs.scala-lang.org\/tour\/polymorphic-methods.html\" target=\"_blank\" rel=\"noopener\">polymorphic method<\/a> that processes either numbers or strings to find the first value in lexicographical order (alphabetically). The naive implementation uses <code>Any<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> findLexicographicalMax<span style=\"color:#f92672\">(<\/span>items<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">List<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> items<span style=\"color:#f92672\">.<\/span>reduce<span style=\"color:#f92672\">((<\/span>a<span style=\"color:#f92672\">,<\/span> b<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>a<span style=\"color:#f92672\">.<\/span>toString <span style=\"color:#f92672\">&gt;<\/span> b<span style=\"color:#f92672\">.<\/span>toString<span style=\"color:#f92672\">)<\/span> a <span style=\"color:#66d9ef\">else<\/span> b<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This compiles but loses type safety: we can accidentally pass mixed types and get a runtime error:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> mixed <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;hello&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">3.14<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>findLexicographicalMax<span style=\"color:#f92672\">(<\/span>mixed<span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Throws RuntimeException at runtime for 3.14 and List(1, 2)\n<\/span><\/span><\/span><\/code><\/pre><\/div><p>Instead, we can use a generic type parameter to work with any type, as long as all elements within the call are the same:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> findLexicographicalMax<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">T<\/span><span style=\"color:#f92672\">](<\/span>items<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">List<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">T<\/span><span style=\"color:#f92672\">])(<\/span><span style=\"color:#66d9ef\">implicit<\/span> ord<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Ordering<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">T<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">T<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> items<span style=\"color:#f92672\">.<\/span>reduce<span style=\"color:#f92672\">(<\/span>ord<span style=\"color:#f92672\">.<\/span>max<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>The function now works with any homogeneous list, and mixed types are caught at compile time:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> strings <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;hello&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;world&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>findLexicographicalMax<span style=\"color:#f92672\">(<\/span>strings<span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Returns &#34;world&#34;\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> numbers <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">3<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>findLexicographicalMax<span style=\"color:#f92672\">(<\/span>numbers<span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Returns 3\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> mixed <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;hello&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Compile error: type mismatch\n<\/span><\/span><\/span><\/code><\/pre><\/div><p>Thanks to Scala 3, we can use the new <a href=\"https:\/\/docs.scala-lang.org\/scala3\/book\/types-union.html\" target=\"_blank\" rel=\"noopener\">union types<\/a> to hold multiple types within the same collection:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> findMaxUnion<span style=\"color:#f92672\">(<\/span>items<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">List<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Int<\/span> <span style=\"color:#66d9ef\">|<\/span> <span style=\"color:#66d9ef\">String<\/span> <span style=\"color:#66d9ef\">|<\/span> <span style=\"color:#66d9ef\">Double<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Int<\/span> <span style=\"color:#66d9ef\">|<\/span> <span style=\"color:#66d9ef\">String<\/span> <span style=\"color:#66d9ef\">|<\/span> <span style=\"color:#66d9ef\">Double<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> items<span style=\"color:#f92672\">.<\/span>maxBy <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> i<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Int<\/span> <span style=\"color:#f92672\">=&gt;<\/span> i<span style=\"color:#f92672\">.<\/span>toString\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> s<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span> <span style=\"color:#f92672\">=&gt;<\/span> s\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> d<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Double<\/span> <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#e6db74\">f&#34;<\/span><span style=\"color:#e6db74\">$d<\/span><span style=\"color:#e6db74\">%.2f&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> mixed <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;hello&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">3.14<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>findMaxUnion<span style=\"color:#f92672\">(<\/span>mixed<span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Returns &#34;hello&#34;\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">val<\/span> mixedUnsupported <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">42<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;hello&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>findMaxUnion<span style=\"color:#f92672\">(<\/span>mixedUnsupported<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Compile error: Required List[Int | String | Double]\n<\/span><\/span><\/span><\/code><\/pre><\/div><h3 id=\"2-more-complex-example\">2. More Complex Example<\/h3>\n<p>Here&rsquo;s a more complex example from my real-world <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\" target=\"_blank\" rel=\"noopener\">Sparklet<\/a> project (a function rebuild of the Spark engine).<\/p>\n<p>In Sparklet, we generate data processing stages based on one of many supported transformations. A naive implementation would use Any to handle different transformation types, which would have the issue of erasing transformation types:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> processOperation<span style=\"color:#f92672\">(<\/span>op<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ We have no idea what operations are valid\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Pattern matching is unsafe and incomplete\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> op <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> f<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Function1<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">_<\/span>, <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> f<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#75715e\">\/\/ Unsafe casting everywhere!\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">throw<\/span> <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">RuntimeException<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;Unknown operation&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Here&rsquo;s a better approach using a sealed trait:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">sealed<\/span> <span style=\"color:#66d9ef\">trait<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Narrow transformations\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">MapOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">](<\/span>f<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">A<\/span> <span style=\"color:#f92672\">=&gt;<\/span> B<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">FilterOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span><span style=\"color:#f92672\">](<\/span>p<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">A<\/span> <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#a6e22e\">Boolean<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">A<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">FlatMapOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">](<\/span>f<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">A<\/span> <span style=\"color:#f92672\">=&gt;<\/span> <span style=\"color:#a6e22e\">IterableOnce<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">])<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Key-value operations\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">KeysOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V<\/span><span style=\"color:#f92672\">]()<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[(<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V<\/span><span style=\"color:#f92672\">)<\/span>, <span style=\"color:#66d9ef\">K<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">ValuesOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V<\/span><span style=\"color:#f92672\">]()<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[(<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V<\/span><span style=\"color:#f92672\">)<\/span>, <span style=\"color:#66d9ef\">V<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">final<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">MapValuesOp<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V<\/span>, <span style=\"color:#66d9ef\">V2<\/span><span style=\"color:#f92672\">](<\/span>f<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">V<\/span> <span style=\"color:#f92672\">=&gt;<\/span> V2<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">extends<\/span> <span style=\"color:#a6e22e\">Operation<\/span><span style=\"color:#f92672\">[(<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V<\/span><span style=\"color:#f92672\">)<\/span>, <span style=\"color:#f92672\">(<\/span><span style=\"color:#66d9ef\">K<\/span>, <span style=\"color:#66d9ef\">V2<\/span><span style=\"color:#f92672\">)]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> processOperation<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">](<\/span>op<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span> <span style=\"color:#f92672\">=<\/span> op <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">s&#34;Map operation with function: <\/span><span style=\"color:#e6db74\">$f<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FilterOp<\/span><span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">s&#34;Filter operation with predicate: <\/span><span style=\"color:#e6db74\">$p<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FlatMapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">s&#34;FlatMap operation with function: <\/span><span style=\"color:#e6db74\">$f<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">KeysOp<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">&#34;Extract keys from key-value pairs&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">ValuesOp<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">&#34;Extract values from key-value pairs&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapValuesOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#e6db74\">s&#34;Map values with function: <\/span><span style=\"color:#e6db74\">$f<\/span><span style=\"color:#e6db74\">&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Link to Sparklet code: <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/0f9be0bf03a99c8721096e014ef64ea501faefe8\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/Operation.scala\" target=\"_blank\" rel=\"noopener\">Operations.scala<\/a><\/p>\n<h3 id=\"3-when-any-is-unavoidable\">3. When Any is Unavoidable<\/h3>\n<p>Here&rsquo;s another example from Sparklet&rsquo;s stage builder, where we handle operation types that were erased to <code>Operation[Any, Any]<\/code> at runtime. There are unsafe casts all throughout the core logic:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> createStageFromOp<span style=\"color:#f92672\">(<\/span>op<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> op <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">KeysOp<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>keys<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">].<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]]<\/span> <span style=\"color:#75715e\">\/\/ Unsafe casting scattered everywhere\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">ValuesOp<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>values<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">].<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]]<\/span> <span style=\"color:#75715e\">\/\/ Hard to audit safety\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapValuesOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>mapValues<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">](<\/span>f<span style=\"color:#f92672\">).<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]]<\/span> <span style=\"color:#75715e\">\/\/ Repetitive and error-prone\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FilterOp<\/span><span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FlatMapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>flatMap<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ ... other cases\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>This is one of the few locations within Sparklet where <code>Any<\/code> is truly unavoidable, because operations with different types (<code>MapOp[String, Int]<\/code> and <code>FilterOp[Double]<\/code>) must be processed together in a single hetergeneous Scala collection. There&rsquo;s no way to keep the types intact throughout the entire stage building operation; we need to use <code>Operation[Any, Any]<\/code> as a common type instead.<\/p>\n<p>We handle this by isolating the <code>Any<\/code> usage to a boundary method, <code>createStageFromOpUnsafe<\/code>; type safety is preserved elsewhere in the class. Ideally, this unsafe method would be subject to focused unit testing, relying on runtime checks in lieu of compile-time safety.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Type-safe stage builders\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">object<\/span> <span style=\"color:#a6e22e\">StageBuilder<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">def<\/span> fromOperation<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">](<\/span>op<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> op <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FilterOp<\/span><span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FlatMapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>flatMap<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">KeysOp<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>keys<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#75715e\">\/\/ Type parameters are preserved in core logic\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">ValuesOp<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>values<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">A<\/span>, <span style=\"color:#66d9ef\">B<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapValuesOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>mapValues<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Ignore WartRemover only for this method\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">@SuppressWarnings<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">Array<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;org.wartremover.warts.Any&#34;<\/span><span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">private<\/span> <span style=\"color:#66d9ef\">def<\/span> createStageFromOpUnsafe<span style=\"color:#f92672\">(<\/span>op<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Single controlled point of type erasure\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">StageBuilder<\/span><span style=\"color:#f92672\">.<\/span>fromOperation<span style=\"color:#f92672\">(<\/span>op<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Lint to Sparklet code: <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\/blob\/main\/modules\/sparklet-execution\/src\/main\/scala\/com\/ewoodbury\/sparklet\/execution\/StageBuilder.scala\" target=\"_blank\" rel=\"noopener\">StageBuilder.scala<\/a><\/p>\n<p>A few other examples of when <code>Any<\/code> is necessary would be when using reflection APIs, serialization\/deserialization like JSON, or interacting with external systems where types are dynamic.<\/p>"},{"title":"Safer Scala with WartRemover Part 1: Seq.apply and head","link":"https:\/\/ewoodbury.com\/posts\/2025-08-25_head_seq_apply_wartremover\/","pubDate":"Mon, 25 Aug 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-08-25_head_seq_apply_wartremover\/","description":"<p>This is the first of a series of posts about using <a href=\"https:\/\/www.wartremover.org\/\" target=\"_blank\" rel=\"noopener\">WartRemover<\/a> to improve the quality of Scala codebases. WartRemover is a static analysis tool for Scala.<\/p>\n<p>I find it useful because it actively disables parts of Scala to improve safety by default. You get the benefits of a stricter language like Rust, while still being able to use <code>SuppressWarnings<\/code> as an escape hatch.<\/p>\n<p>I also like that the warnings often present as a mini programming puzzle. Will I be clever enough to fix it, perhaps even without AI help?<\/p>\n<hr>\n<p>Here&rsquo;s a very short reference on setting up WartRemover for your Scala project:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ plugins.sbt\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>addSbtPlugin<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;org.wartremover&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;sbt-wartremover&#34;<\/span> <span style=\"color:#f92672\">%<\/span> <span style=\"color:#e6db74\">&#34;3.3.3&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ any other plugins...\n<\/span><\/span><\/span><\/code><\/pre><\/div><div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ build.sbt\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Set Warts as warnings (compile can still succeed)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>wartremoverWarnings <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#a6e22e\">WartRemover<\/span><span style=\"color:#f92672\">.<\/span>warts<span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Any<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Or set Warts as errors (compile fails)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>wartremoverErrors <span style=\"color:#f92672\">+=<\/span> <span style=\"color:#a6e22e\">Wart<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">SeqApply<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Or if you want to include all warts with the exception of a few:\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>wartremoverWarnings <span style=\"color:#f92672\">++=<\/span> <span style=\"color:#a6e22e\">Warts<\/span><span style=\"color:#f92672\">.<\/span>allBut<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Wart<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">ImplicitParameter<\/span><span style=\"color:#f92672\">,<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Check out the <a href=\"https:\/\/www.wartremover.org\/doc\/install-setup.html\" target=\"_blank\" rel=\"noopener\">WartRemover Setup Guide<\/a> for more details.<\/p>\n<hr>\n<p>Two closely-related Warts are <code>SeqIndex<\/code> and <code>Head<\/code>. These Warts ban the use of <code>Seq.apply<\/code> (i.e. indexing into a sequence with parentheses) and <code>Seq.head<\/code>, since both can throw exceptions at runtime.<\/p>\n<h3 id=\"basic-example\">Basic Example<\/h3>\n<p>Here&rsquo;s an example of real code from my <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\" target=\"_blank\" rel=\"noopener\">Sparklet<\/a> project that was caught by this Wart:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">private<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">execution<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">def<\/span> materialize<span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> ops<span style=\"color:#f92672\">.<\/span>head <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span> <span style=\"color:#75715e\">\/\/ WartRemover warning raised: .head is disabled\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">])<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FilterOp<\/span><span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">Boolean<\/span><span style=\"color:#f92672\">])<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FlatMapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>flatMap<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">IterableOnce<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]])<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>The fix is simple for this case: check that the sequence is non-empty, and then use <code>headOption.get<\/code>, which is safe because of the prior check.<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">private<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">execution<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">def<\/span> materialize<span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Operation<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Stage<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span>, <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;Cannot materialize empty operation vector&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">if<\/span> <span style=\"color:#f92672\">(<\/span>ops<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">return<\/span> ops<span style=\"color:#f92672\">.<\/span>headOption<span style=\"color:#f92672\">.<\/span>get <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">MapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">])<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FilterOp<\/span><span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>filter<span style=\"color:#f92672\">(<\/span>p<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">Boolean<\/span><span style=\"color:#f92672\">])<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">FlatMapOp<\/span><span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#a6e22e\">Stage<\/span><span style=\"color:#f92672\">.<\/span>flatMap<span style=\"color:#f92672\">(<\/span>f<span style=\"color:#f92672\">.<\/span>asInstanceOf<span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> <span style=\"color:#66d9ef\">IterableOnce<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">Any<\/span><span style=\"color:#f92672\">]])<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">...<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Similarly, below is an example of replacing <code>Seq.apply<\/code> with safe alternatives:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> shuffleInputSources <span style=\"color:#66d9ef\">=<\/span> meta<span style=\"color:#f92672\">.<\/span>kind <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Join<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">CoGroup<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Indexing with meta.sides(0) and meta.sides(1) uses Seq.apply;\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ this is unsafe if sides has less than 2 elements\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">0<\/span><span style=\"color:#f92672\">),<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>meta<span style=\"color:#f92672\">.<\/span>sides<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">0<\/span><span style=\"color:#f92672\">)),<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">),<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>meta<span style=\"color:#f92672\">.<\/span>sides<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">)),<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Single-input operations\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">1<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>meta<span style=\"color:#f92672\">.<\/span>kind<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> requires exactly 1 upstream stage&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">.<\/span>head<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">None<\/span><span style=\"color:#f92672\">,<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">))<\/span> <span style=\"color:#75715e\">\/\/ using head\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>We can avoid the apply by first checking the input sequence with <code>require<\/code>, and then using <code>take<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> shuffleInputSources <span style=\"color:#66d9ef\">=<\/span> meta<span style=\"color:#f92672\">.<\/span>kind <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">Join<\/span> <span style=\"color:#f92672\">|<\/span> <span style=\"color:#a6e22e\">WideOpKind<\/span><span style=\"color:#f92672\">.<\/span><span style=\"color:#a6e22e\">CoGroup<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>meta<span style=\"color:#f92672\">.<\/span>kind<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> requires exactly 2 upstream stages&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>meta<span style=\"color:#f92672\">.<\/span>sides<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> <span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>meta<span style=\"color:#f92672\">.<\/span>kind<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> requires exactly 2 sides&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span>upstream1<span style=\"color:#f92672\">,<\/span> upstream2<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> upstreamIds<span style=\"color:#f92672\">.<\/span>take<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">).<\/span>toList\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> <span style=\"color:#a6e22e\">List<\/span><span style=\"color:#f92672\">(<\/span>side1<span style=\"color:#f92672\">,<\/span> side2<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=<\/span> meta<span style=\"color:#f92672\">.<\/span>sides<span style=\"color:#f92672\">.<\/span>take<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">2<\/span><span style=\"color:#f92672\">).<\/span>toList\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstream1<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>side1<span style=\"color:#f92672\">),<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstream2<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Some<\/span><span style=\"color:#f92672\">(<\/span>side2<span style=\"color:#f92672\">),<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Single-input operations\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> require<span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">.<\/span>nonEmpty<span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">s&#34;<\/span><span style=\"color:#e6db74\">${<\/span>meta<span style=\"color:#f92672\">.<\/span>kind<span style=\"color:#e6db74\">}<\/span><span style=\"color:#e6db74\"> requires exactly 1 upstream stage&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Seq<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">ShuffleInput<\/span><span style=\"color:#f92672\">(<\/span>upstreamIds<span style=\"color:#f92672\">.<\/span>headOption<span style=\"color:#f92672\">.<\/span>get<span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">None<\/span><span style=\"color:#f92672\">,<\/span> meta<span style=\"color:#f92672\">.<\/span>numPartitions<span style=\"color:#f92672\">))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Of course, the <code>require<\/code> can still throw the exception for an emtpy sequence, but this fails fast and is easier to debug than a <code>NoSuchElementException<\/code>.<\/p>\n<h3 id=\"more-complex-example\">More Complex Example<\/h3>\n<p>The value is perhaps best demonstrated in a more complex case, where having WartRemover restrict <code>head<\/code> and <code>apply<\/code> forces a larger refactor, improving safety significantly.<\/p>\n<p>Consider this Spark pipeline:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> org.apache.spark.sql.<span style=\"color:#f92672\">{<\/span><span style=\"color:#a6e22e\">DataFrame<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#a6e22e\">Column<\/span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">import<\/span> org.apache.spark.sql.functions._\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#66d9ef\">class<\/span> <span style=\"color:#a6e22e\">JoinSpec<\/span><span style=\"color:#f92672\">(<\/span>leftCol<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span> rightCol<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">,<\/span> joinType<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> buildDataPipeline<span style=\"color:#f92672\">(<\/span>tables<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">DataFrame<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> joinSpecs<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">JoinSpec<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> groupByCols<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">])<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">DataFrame<\/span> <span style=\"color:#f92672\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Unsafe: could throw if empty\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> baseDf <span style=\"color:#66d9ef\">=<\/span> tables<span style=\"color:#f92672\">.<\/span>head\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> joinDfs <span style=\"color:#66d9ef\">=<\/span> tables<span style=\"color:#f92672\">.<\/span>tail\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Build joins - unsafe indexing\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> joinedDf <span style=\"color:#66d9ef\">=<\/span> joinSpecs<span style=\"color:#f92672\">.<\/span>zipWithIndex<span style=\"color:#f92672\">.<\/span>foldLeft<span style=\"color:#f92672\">(<\/span>baseDf<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#f92672\">(<\/span>df<span style=\"color:#f92672\">,<\/span> <span style=\"color:#f92672\">(<\/span>spec<span style=\"color:#f92672\">,<\/span> idx<span style=\"color:#f92672\">))<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> df<span style=\"color:#f92672\">.<\/span>join<span style=\"color:#f92672\">(<\/span>joinDfs<span style=\"color:#f92672\">(<\/span>idx<span style=\"color:#f92672\">),<\/span> col<span style=\"color:#f92672\">(<\/span>spec<span style=\"color:#f92672\">.<\/span>leftCol<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">===<\/span> col<span style=\"color:#f92672\">(<\/span>spec<span style=\"color:#f92672\">.<\/span>rightCol<span style=\"color:#f92672\">),<\/span> spec<span style=\"color:#f92672\">.<\/span>joinType<span style=\"color:#f92672\">)<\/span> <span style=\"color:#75715e\">\/\/ Unsafe indexing\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Group by - unsafe head access\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> primaryGroupCol <span style=\"color:#66d9ef\">=<\/span> groupByCols<span style=\"color:#f92672\">.<\/span>head <span style=\"color:#75715e\">\/\/ Unsafe\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> restGroupCols <span style=\"color:#66d9ef\">=<\/span> groupByCols<span style=\"color:#f92672\">.<\/span>tail<span style=\"color:#f92672\">.<\/span>map<span style=\"color:#f92672\">(<\/span>col<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> joinedDf\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">.<\/span>groupBy<span style=\"color:#f92672\">(<\/span>col<span style=\"color:#f92672\">(<\/span>primaryGroupCol<span style=\"color:#f92672\">),<\/span> restGroupCols<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#66d9ef\">*<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">.<\/span>agg<span style=\"color:#f92672\">(<\/span>count<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;*&#34;<\/span><span style=\"color:#f92672\">).<\/span>as<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;record_count&#34;<\/span><span style=\"color:#f92672\">),<\/span> sum<span style=\"color:#f92672\">(<\/span>col<span style=\"color:#f92672\">(<\/span>groupByCols<span style=\"color:#f92672\">(<\/span><span style=\"color:#ae81ff\">0<\/span><span style=\"color:#f92672\">))).<\/span>as<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;total&#34;<\/span><span style=\"color:#f92672\">))<\/span> <span style=\"color:#75715e\">\/\/ Unsafe indexing\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>We can improve this by removing the unsafe <code>head<\/code> and <code>apply<\/code> indexing cols, and by using a functional for-comprehension with <code>Either<\/code> to handle runtime errors cleanly:<\/p>\n<!-- ```scala\nimport org.apache.spark.sql.{DataFrame, Column}\nimport org.apache.spark.sql.functions._\ncase class JoinSpec(leftCol: String, rightCol: String, joinType: String)\ndef buildDataPipeline(tables: Vector[DataFrame],\njoinSpecs: Vector[JoinSpec],\ngroupByCols: Vector[String]): Either[String, DataFrame] = {\n\/\/ Functional for-comprehension\n\/\/ Helper functions use `Either` to enable safe error handling\nfor {\nbaseDf <- tables.headOption.toRight(\"Pipeline requires at least one DataFrame\")\njoinedDf <- applyJoins(baseDf, tables.tail, joinSpecs)\nresult <- applyGrouping(joinedDf, groupByCols)\n} yield result\n}\n``` -->\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-scala\" data-lang=\"scala\"><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Safe, concise implementation\n<\/span><\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">def<\/span> buildDataPipeline<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> tables<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">DataFrame<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> joinSpecs<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">JoinSpec<\/span><span style=\"color:#f92672\">],<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> groupByCols<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Vector<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span><span style=\"color:#f92672\">]<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span><span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">Either<\/span><span style=\"color:#f92672\">[<\/span><span style=\"color:#66d9ef\">String<\/span>, <span style=\"color:#66d9ef\">DataFrame<\/span><span style=\"color:#f92672\">]<\/span> <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">for<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> baseDf <span style=\"color:#66d9ef\">&lt;-<\/span> tables<span style=\"color:#f92672\">.<\/span>headOption<span style=\"color:#f92672\">.<\/span>toRight<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;Pipeline requires at least one DataFrame&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">_<\/span> <span style=\"color:#66d9ef\">&lt;-<\/span> <span style=\"color:#a6e22e\">Either<\/span><span style=\"color:#f92672\">.<\/span>cond<span style=\"color:#f92672\">(<\/span>tables<span style=\"color:#f92672\">.<\/span>tail<span style=\"color:#f92672\">.<\/span>length <span style=\"color:#f92672\">==<\/span> joinSpecs<span style=\"color:#f92672\">.<\/span>length<span style=\"color:#f92672\">,<\/span> <span style=\"color:#f92672\">(),<\/span> <span style=\"color:#e6db74\">&#34;Mismatch: tables and join specs&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Safe zip-based joining (no indexing needed)\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> joinedDf <span style=\"color:#66d9ef\">=<\/span> tables<span style=\"color:#f92672\">.<\/span>tail<span style=\"color:#f92672\">.<\/span>zip<span style=\"color:#f92672\">(<\/span>joinSpecs<span style=\"color:#f92672\">).<\/span>foldLeft<span style=\"color:#f92672\">(<\/span>baseDf<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">{<\/span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#f92672\">(<\/span>df<span style=\"color:#f92672\">,<\/span> <span style=\"color:#f92672\">(<\/span>joinDf<span style=\"color:#f92672\">,<\/span> spec<span style=\"color:#f92672\">))<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> df<span style=\"color:#f92672\">.<\/span>join<span style=\"color:#f92672\">(<\/span>joinDf<span style=\"color:#f92672\">,<\/span> col<span style=\"color:#f92672\">(<\/span>spec<span style=\"color:#f92672\">.<\/span>leftCol<span style=\"color:#f92672\">)<\/span> <span style=\"color:#f92672\">===<\/span> col<span style=\"color:#f92672\">(<\/span>spec<span style=\"color:#f92672\">.<\/span>rightCol<span style=\"color:#f92672\">),<\/span> spec<span style=\"color:#f92672\">.<\/span>joinType<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#75715e\">\/\/ Safe pattern matching for grouping\n<\/span><\/span><\/span><span style=\"display:flex;\"><span> result <span style=\"color:#66d9ef\">&lt;-<\/span> groupByCols <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Vector<\/span><span style=\"color:#f92672\">()<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Right<\/span><span style=\"color:#f92672\">(<\/span>joinedDf<span style=\"color:#f92672\">.<\/span>agg<span style=\"color:#f92672\">(<\/span>count<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;*&#34;<\/span><span style=\"color:#f92672\">).<\/span>as<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;record_count&#34;<\/span><span style=\"color:#f92672\">)))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> single <span style=\"color:#f92672\">+:<\/span> rest <span style=\"color:#66d9ef\">=&gt;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">val<\/span> cols <span style=\"color:#66d9ef\">=<\/span> <span style=\"color:#f92672\">(<\/span>single <span style=\"color:#f92672\">+:<\/span> rest<span style=\"color:#f92672\">).<\/span>map<span style=\"color:#f92672\">(<\/span>col<span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Right<\/span><span style=\"color:#f92672\">(<\/span>joinedDf<span style=\"color:#f92672\">.<\/span>groupBy<span style=\"color:#f92672\">(<\/span>cols<span style=\"color:#66d9ef\">:<\/span> <span style=\"color:#66d9ef\">_<\/span><span style=\"color:#66d9ef\">*<\/span><span style=\"color:#f92672\">).<\/span>agg<span style=\"color:#f92672\">(<\/span>count<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;*&#34;<\/span><span style=\"color:#f92672\">).<\/span>as<span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;count&#34;<\/span><span style=\"color:#f92672\">)))<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">}<\/span> <span style=\"color:#66d9ef\">yield<\/span> result\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#75715e\">\/\/ Clean usage\n<\/span><\/span><\/span><span style=\"display:flex;\"><span>buildDataPipeline<span style=\"color:#f92672\">(<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Vector<\/span><span style=\"color:#f92672\">(<\/span>usersDf<span style=\"color:#f92672\">,<\/span> ordersDf<span style=\"color:#f92672\">),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Vector<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#a6e22e\">JoinSpec<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;user_id&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;user_id&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;inner&#34;<\/span><span style=\"color:#f92672\">)),<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#a6e22e\">Vector<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">&#34;department&#34;<\/span><span style=\"color:#f92672\">,<\/span> <span style=\"color:#e6db74\">&#34;status&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">match<\/span> <span style=\"color:#f92672\">{<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Left<\/span><span style=\"color:#f92672\">(<\/span>error<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> raise <span style=\"color:#66d9ef\">new<\/span> <span style=\"color:#a6e22e\">RuntimeException<\/span><span style=\"color:#f92672\">(<\/span><span style=\"color:#e6db74\">s&#34;Pipeline failed due to invalid job spec: <\/span><span style=\"color:#e6db74\">$error<\/span><span style=\"color:#e6db74\">&#34;<\/span><span style=\"color:#f92672\">)<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span> <span style=\"color:#66d9ef\">case<\/span> <span style=\"color:#a6e22e\">Right<\/span><span style=\"color:#f92672\">(<\/span>df<span style=\"color:#f92672\">)<\/span> <span style=\"color:#66d9ef\">=&gt;<\/span> df<span style=\"color:#f92672\">.<\/span>show<span style=\"color:#f92672\">()<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>I plan to write about functional for-comprehensions in a future post, as I find them an interesting topic as well!<\/p>"},{"title":"Rebuilding Spark with Scala 3","link":"https:\/\/ewoodbury.com\/posts\/2025-07-31_sparklet\/","pubDate":"Thu, 31 Jul 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-07-31_sparklet\/","description":"<p>Github Link: <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\" target=\"_blank\" rel=\"noopener\">Sparklet<\/a><\/p>\n<p>Over the last two years at Tesla, I&rsquo;ve spent about 80% of my coding time in Spark. My team has moved a 50K-ish LOC data pipeline codebase from un-tested Python code to type-safe, fully-tested Scala Spark code. We&rsquo;ve built about a dozen new, high-volume data pipelines in this project as well.<\/p>\n<p>And&hellip;it&rsquo;s been fun! This was the first opportunity in years that I&rsquo;ve been able to focus on code quality and architecture. I set out with the goal of building pipelines that are 100% correct over billion-row datasets and stable enough to run in factories for years to come, and my team has achieved that goal.<\/p>\n<p>The experience has also sparked (wink) my interest in data processing internals. As a result, I&rsquo;ve started a project to rebuild the Spark core in Scala 3.<\/p>\n<h2 id=\"current-state\">Current State<\/h2>\n<p>The project is in early stages, but the foundations are in place:<\/p>\n<ul>\n<li>Scala 3 for modern language features and best practices<\/li>\n<li>Planning of tasks and stages<\/li>\n<li>Lazy execution<\/li>\n<li>Basic user API<\/li>\n<li>Simulated cluster execution using local threads<\/li>\n<li>Effects system for task execution with cats-effect<\/li>\n<li>Full test coverage<\/li>\n<\/ul>\n<p>My current goal is to finish implementing a full processing core, plus a DataFrame and DataSet API. From there, I&rsquo;d like to run full data pipelines representative of a production Spark job.<\/p>\n<h2 id=\"why\">Why?<\/h2>\n<p>Of course, Spark isn&rsquo;t going anywhere.<\/p>\n<p>But, it&rsquo;s still using Scala 2. Planning takes longer than it should. The web monitoring UI is a bit clunky and dated. There could be improvements in how query plans are displayed.<\/p>\n<p>I&rsquo;d like to build a proof of what a modern Spark could look like.<\/p>\n<p>Selfishly, I want to gain experience building distributed systems and learning about how Spark really works.<\/p>\n<h2 id=\"alternatives\">Alternatives<\/h2>\n<p>I&rsquo;d be remiss not to mention the many other modern alternatives to Spark that already exist.<\/p>\n<p>The most similar ones are those that maintain a focus on batch processing while support distributed execution out of the box. <a href=\"https:\/\/github.com\/apache\/datafusion\" target=\"_blank\" rel=\"noopener\">Apache DataFusion<\/a> is fresh data engine in Rust, and <a href=\"https:\/\/github.com\/apache\/datafusion-ballista\" target=\"_blank\" rel=\"noopener\">DataFusion Ballista<\/a> is the execution framework that enables writing actual jobs. <a href=\"https:\/\/docs.ray.io\/en\/latest\/data\/data.html\" target=\"_blank\" rel=\"noopener\">Ray<\/a> is another distributed compute framework, although it&rsquo;s focused more on ML training workloads. Lastly, <a href=\"https:\/\/github.com\/dask\/dask\" target=\"_blank\" rel=\"noopener\">Dask<\/a> is a mature Python framework for the same, although personally I&rsquo;ve found it a bit clunky.<\/p>\n<p>(Edit 9\/4): <a href=\"https:\/\/github.com\/pola-rs\/polars\" target=\"_blank\" rel=\"noopener\">Polars<\/a>, the company, <a href=\"https:\/\/pola.rs\/posts\/polars-cloud-launch\/\" target=\"_blank\" rel=\"noopener\">just launched<\/a> a distributed compute offering on AWS using the <code>polars<\/code> engine. I&rsquo;ve enjoyed polars from my limited local usage, and it has a good reputation in the community, so I could see this gaining steam as well.<\/p>\n<p>In the future, I&rsquo;d like to explore contributing to Datafusion and Ballista, which I think have the most potential to grow into a full Spark competitor.<\/p>\n<p>That all being said, I&rsquo;ve been having a lot of fun with building and owning my own project end-to-end for now, and I hope to share more as it progresses. Check it out on <a href=\"https:\/\/github.com\/ewoodbury\/sparklet\" target=\"_blank\" rel=\"noopener\">GitHub<\/a>!<\/p>"},{"title":"A simple Git alias setup","link":"https:\/\/ewoodbury.com\/posts\/2025-05-02_git_aliases\/","pubDate":"Fri, 02 May 2025 00:00:00 +0000","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/2025-05-02_git_aliases\/","description":"<p>Here are a few simple but useful Git shortcuts that I use dozens of times a day.<\/p>\n<p>TLDR: Add these to your <code>.bashrc<\/code> or <code>.zshrc<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>alias g<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#39;git&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>alias gs<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#39;git status -sb&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>alias glog<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#34;git log --graph --pretty=format:&#39;%Cred%h%Creset %an: %s - %Creset %C(yellow)%d%Creset %Cgreen(%cr)%Creset&#39; --abbrev-commit --date=relative&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>alias ga<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#39;git add&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>alias gc<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#39;git commit -m&#39;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>alias gp<span style=\"color:#f92672\">=<\/span><span style=\"color:#e6db74\">&#39;git push&#39;<\/span>\n<\/span><\/span><\/code><\/pre><\/div><h3 id=\"1-gs-status\">1. <code>gs<\/code>: Status<\/h3>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>gs\n<\/span><\/span><\/code><\/pre><\/div><p><img src=\"image.png\" alt=\"gs output\"><\/p>\n<p>I prefer this condensed status view over the default, but remove the <code>-sb<\/code> if you want the full version.<\/p>\n<h3 id=\"2-glog-log-graph\">2. <code>glog<\/code>: Log Graph<\/h3>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>glog\n<\/span><\/span><\/code><\/pre><\/div><p><img src=\"image-1.png\" alt=\"alt text\"><\/p>\n<h3 id=\"3-gagcgp-addcommitpush\">3. <code>ga\/gc\/gp<\/code>: Add\/Commit\/Push<\/h3>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>ga content\/posts\/\n<\/span><\/span><span style=\"display:flex;\"><span>gc <span style=\"color:#e6db74\">&#34;Add git shortcuts post&#34;<\/span>\n<\/span><\/span><span style=\"display:flex;\"><span>gp\n<\/span><\/span><\/code><\/pre><\/div><p><img src=\"image-2.png\" alt=\"alt text\"><\/p>\n<h3 id=\"4-g-short-for-git\">4. <code>g<\/code>: Short for <code>git<\/code><\/h3>\n<p>Use for any other git command<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>g stash\n<\/span><\/span><\/code><\/pre><\/div><p>There are many online resources with more comprehensive setups, but the ones below strike the right balance for me. Others that are interesting:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.durdn.com\/blog\/2012\/11\/22\/must-have-git-aliases-advanced-examples\/\" target=\"_blank\" rel=\"noopener\">Must-Have Git Aliases<\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/mwhite\/6887990\" target=\"_blank\" rel=\"noopener\">Ultimate Git Alias Setup<\/a><\/li>\n<\/ul>"},{"title":"Introduction","link":"https:\/\/ewoodbury.com\/posts\/introduction\/","pubDate":"Sun, 22 Jan 2023 11:53:25 -0800","author":"ethwoodbury@gmail.com (Ethan Woodbury)","guid":"https:\/\/ewoodbury.com\/posts\/introduction\/","description":"<p>Welcome to my website! I plan to post about various topics that interest me, including programming, data analytics, databases, and how data is used in organizations.<\/p>\n<p>I may also write about non-programming topics that interest me. This might include global development, effective altruism, financial markets, cycling, and photography.<\/p>\n<p>Here&rsquo;s a bit more about me, copied from the About tab:<\/p>\n<h2 id=\"about\">About<\/h2>\n<p>I work on Tesla&rsquo;s Cell Manufacturing team, where I lead a team that builds software to enable more efficient manufacturing operations. My team runs the data platform reponsible for <a href=\"https:\/\/x.com\/Tesla\/status\/1798472011678966221\" target=\"_blank\" rel=\"noopener\">factory reporting<\/a> and <a href=\"https:\/\/electrek.co\/2024\/04\/24\/tesla-expects-4680-battery-cells-cheaper-than-suppliers-end-of-year\/\" target=\"_blank\" rel=\"noopener\">financial tracking<\/a>.<\/p>\n<p>I hope that my current work will pull forward the day when the world has access to cheap, abundant energy storage.<\/p>\n<p>My technical interests include analytical data systems (particularly Spark and data lakes), stream processing, data validation and verifiability, and data visualization. I enjoy thinking about how data tools can improve operations in large organizations.<\/p>\n<figure><img src=\"https:\/\/ewoodbury.com\/profile.jpg\">\n<\/figure>\n<p>I previously studied Chemical Engineering at UC Berkeley. My academic background is in materials research for high-capacity, nickel-rich battery cathodes.<\/p>\n<p>Outside of work, I&rsquo;m interested in global development, particularly how to most effectively lift out of poverty the <a href=\"https:\/\/data.worldbank.org\/indicator\/SI.POV.DDAY\" target=\"_blank\" rel=\"noopener\">650 million people living on less than $2.15 per day<\/a>. I&rsquo;m also broadly interested in the <a href=\"https:\/\/forum.effectivealtruism.org\/posts\/rJRw78oihoT5paFGd\/high-level-hopes-for-ai-alignment\" target=\"_blank\" rel=\"noopener\">safety and alignment of AI systems<\/a>.<\/p>\n<p>Away from the keyboard, I enjoy road cycling, mountain biking, running, hiking\/backpacking, and photography.<\/p>\n<p>I live in the Bay Area, where I was also born and raised.<\/p>"}]}}