<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Maksim Martianov's engineering blog]]></title><description><![CDATA[go, functional programming, scala, programming, programming languages, gala]]></description><link>https://martianov.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 20:08:53 GMT</lastBuildDate><atom:link href="https://martianov.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Type Information Problem]]></title><description><![CDATA[If you're building a language that compiles to native code, the path to editor tooling is well-understood: run your type checker, expose the results. But if your language transpiles to another high-level language, you're stuck in an awkward middle gr...]]></description><link>https://martianov.dev/lsp-type-information-problem</link><guid isPermaLink="true">https://martianov.dev/lsp-type-information-problem</guid><category><![CDATA[compilers]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[type systems]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Fri, 10 Apr 2026 03:32:12 GMT</pubDate><content:encoded><![CDATA[<p>If you're building a language that compiles to native code, the path to editor tooling is well-understood: run your type checker, expose the results. But if your language <em>transpiles</em> to another high-level language, you're stuck in an awkward middle ground. Your source language has types. Your target language has types. And they don't talk to each other.</p>
<p>GALA transpiles to Go. When a user writes <code>val x = Some(42)</code> in their editor, the LSP needs to know that <code>x</code> is <code>Option[int]</code>. But there's no Go source file to analyze yet -- the transpiler hasn't run. And even after it runs, the generated Go uses different names, wraps values in <code>Immutable[T]</code>, and structures code in ways that don't map cleanly back to the original source positions. This is the type information gap, and every transpiled language has to solve it.</p>
<h2 id="heading-the-naive-approach-running-gotypes-on-the-output">The naive approach: running go/types on the output</h2>
<p>The first thing you'd try -- and the first thing we tried -- is to transpile the file, then run <code>go/types</code> on the generated Go AST. Go has excellent type-checking infrastructure; why not leverage it?</p>
<p>It works, sort of. You get types, but they're Go types, not GALA types. A GALA <code>val name = "hello"</code> becomes a Go <code>name := std.NewImmutable[string]("hello")</code>, and <code>go/types</code> tells you the type is <code>std.Immutable[string]</code>. Now you need a reverse-mapping layer to turn that back into <code>string</code> for display. A GALA <code>Option[int]</code> becomes <code>std.Option[int]</code> in Go. A sealed type variant with three fields generates a struct with a <code>_variant uint8</code> discriminator. Every generated pattern needs a corresponding un-generation rule.</p>
<p>Worse, <code>go/types</code> needs complete, compilable Go code. If the user is mid-edit and the transpiler produces a partial or broken Go file, the type checker fails entirely -- no partial results, no graceful degradation. For an LSP that needs to respond to every keystroke, "all or nothing" is the wrong tradeoff.</p>
<p>We needed a different approach.</p>
<h2 id="heading-the-solution-the-transpiler-already-knows">The solution: the transpiler already knows</h2>
<p>Here's the insight that changed the design: the transpiler <em>already resolves every type</em> during transformation. When it encounters <code>val x = Some(42)</code>, it infers that <code>Some(42)</code> produces <code>Option[int]</code>, records that in its internal scope, and uses that information to generate the correct Go code. The type information exists -- it's just trapped inside the transformer's mutable state, discarded after the Go AST is emitted.</p>
<p>The fix was surgical. We added a parallel data structure to the transformer that records every variable's resolved type as a side effect of the normal transformation process. No separate analysis pass, no reverse mapping, no additional type checker. The transpiler does its job and the LSP reads the results.</p>
<p>The core of it lives in <code>scope.go</code>:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(t *galaASTTransformer)</span> <span class="hljs-title">addVal</span><span class="hljs-params">(name <span class="hljs-keyword">string</span>, typeName transpiler.Type)</span></span> {
    <span class="hljs-keyword">if</span> t.currentScope != <span class="hljs-literal">nil</span> {
        t.currentScope.vals[name] = <span class="hljs-literal">true</span>
        t.currentScope.valTypes[name] = typeName
    }
    t.recordLSPVarType(name, typeName)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(t *galaASTTransformer)</span> <span class="hljs-title">recordLSPVarType</span><span class="hljs-params">(name <span class="hljs-keyword">string</span>, typeName transpiler.Type)</span></span> {
    <span class="hljs-keyword">if</span> t.lspVarTypes == <span class="hljs-literal">nil</span> || typeName == <span class="hljs-literal">nil</span> || typeName.IsNil() {
        <span class="hljs-keyword">return</span>
    }
    key := name
    <span class="hljs-keyword">if</span> t.lspCurrentFunc != <span class="hljs-string">""</span> {
        key = t.lspCurrentFunc + <span class="hljs-string">"."</span> + name
    }
    t.lspVarTypes[key] = typeName
}
</code></pre>
<p>Every call to <code>addVal</code> or <code>addVar</code> -- the same functions the transpiler uses to track variables for code generation -- now also records the type into <code>lspVarTypes</code>. The recording is conditional: if <code>lspVarTypes</code> is nil (normal compilation, not an LSP invocation), the function returns immediately. Zero overhead for the non-LSP path.</p>
<h2 id="heading-how-types-are-scoped">How types are scoped</h2>
<p>The scoping scheme is deliberately simple. A variable inside a function is keyed as <code>funcName.varName</code>. A top-level variable is keyed as just <code>varName</code>. That's it.</p>
<p>The transformer tracks the current function name via <code>lspCurrentFunc</code>, set at the entry of each function declaration and restored on exit:</p>
<pre><code class="lang-go"><span class="hljs-comment">// In declarations.go, inside the function declaration handler:</span>
prevFunc := t.lspCurrentFunc
t.lspCurrentFunc = name
<span class="hljs-keyword">defer</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> { t.lspCurrentFunc = prevFunc }()
</code></pre>
<p>This means if you have a function <code>calculateTotal</code> with a local <code>val tax = price * 0.1</code>, the key in <code>lspVarTypes</code> is <code>"calculateTotal.tax"</code>. A top-level <code>val config = loadConfig()</code> is just <code>"config"</code>.</p>
<p>On the LSP side, <code>lookupVarType</code> mirrors this convention. It takes the enclosing function name (determined by scanning backwards from the cursor position for a <code>func</code> declaration) and tries the scoped key first, falling back to unscoped:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">lookupVarType</span><span class="hljs-params">(varTypeMap <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>, funcName, varName <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">if</span> varTypeMap == <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>
    }
    <span class="hljs-keyword">if</span> funcName != <span class="hljs-string">""</span> {
        <span class="hljs-keyword">if</span> typStr, ok := varTypeMap[funcName+<span class="hljs-string">"."</span>+varName]; ok {
            <span class="hljs-keyword">return</span> typStr
        }
    }
    <span class="hljs-keyword">if</span> typStr, ok := varTypeMap[varName]; ok {
        <span class="hljs-keyword">return</span> typStr
    }
    <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>
}
</code></pre>
<p>This doesn't handle shadowing within nested scopes (a variable in an inner block with the same name as one in the outer function body), and that's a conscious trade-off. Full scope tracking would require mapping source positions to AST nodes, which adds complexity for a case that rarely matters in practice.</p>
<h2 id="heading-the-lsp-pipeline-from-keystrokes-to-cached-types">The LSP pipeline: from keystrokes to cached types</h2>
<p>The <code>TransformForLSP</code> method is the bridge between the transpiler and the LSP. It calls the regular <code>Transform</code>, then copies out the accumulated <code>lspVarTypes</code>:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(t *galaASTTransformer)</span> <span class="hljs-title">TransformForLSP</span><span class="hljs-params">(richAST *transpiler.RichAST)</span> <span class="hljs-params">(*transpiler.TransformResult, error)</span></span> {
    fset, file, transformErr := t.Transform(richAST)
    <span class="hljs-comment">// Always return collected var types, even on error (partial results)</span>
    varTypes := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]transpiler.Type, <span class="hljs-built_in">len</span>(t.lspVarTypes))
    <span class="hljs-keyword">for</span> name, typ := <span class="hljs-keyword">range</span> t.lspVarTypes {
        varTypes[name] = typ
    }
    <span class="hljs-keyword">return</span> &amp;transpiler.TransformResult{
        Fset:     fset,
        File:     file,
        VarTypes: varTypes,
    }, transformErr
}
</code></pre>
<p>Note the comment: "even on error (partial results)." If the transpiler fails halfway through a file -- because the user is still typing -- we still get types for every variable processed before the error. This is the critical difference from the <code>go/types</code> approach. Partial results are the default, not a special case.</p>
<p>The LSP server calls this inside <code>analyzeFile</code>, which runs on every document change (after a 300ms debounce). The returned <code>VarTypes</code> map gets cleaned up and cached:</p>
<pre><code class="lang-go"><span class="hljs-keyword">if</span> result != <span class="hljs-literal">nil</span> &amp;&amp; result.VarTypes != <span class="hljs-literal">nil</span> {
    typeMap := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>, <span class="hljs-built_in">len</span>(result.VarTypes))
    <span class="hljs-keyword">for</span> name, typ := <span class="hljs-keyword">range</span> result.VarTypes {
        typeMap[name] = cleanGoTypeForDisplay(typ.String())
    }
    h.mu.Lock()
    h.varTypes[uri] = typeMap
    h.mu.Unlock()
}
</code></pre>
<p>The <code>cleanGoTypeForDisplay</code> function handles the impedance mismatch between GALA's internal type representation and what a user expects to see. It strips <code>std.</code> prefixes and unwraps <code>Immutable[T]</code> to <code>T</code> -- the generated Go uses <code>Immutable</code> wrappers to enforce value semantics on <code>val</code> bindings, but the user doesn't need to know that.</p>
<h2 id="heading-what-this-enables">What this enables</h2>
<p>With a single <code>map[string]string</code> per open document, the LSP gets:</p>
<p><strong>Inlay hints.</strong> When the user writes <code>val x = someExpression()</code>, the editor shows <code>: ReturnType</code> as a ghost annotation after <code>x</code>. The inlay hint handler walks visible lines, matches <code>val</code>/<code>var</code> declarations without explicit types, and looks up the variable name in <code>varTypes</code>.</p>
<p><strong>Dot completion.</strong> When the user types <code>x.</code>, <code>typeAtDot</code> resolves <code>x</code> to its type via the same <code>lookupVarType</code> function, then looks up that type's methods and fields in the <code>RichAST</code> to build the completion list.</p>
<p><strong>Hover information.</strong> Same lookup, different presentation -- show the type in a hover tooltip instead of a completion menu.</p>
<p>All three features share the exact same type resolution path. There's no separate type-checking logic for completions vs. hints vs. hover. If the transpiler resolves a type, every LSP feature sees it.</p>
<h2 id="heading-the-trade-off">The trade-off</h2>
<p>This design couples the LSP tightly to the transpiler's internals. If the transpiler changes how it represents types, every LSP feature is affected. But the alternative -- maintaining a separate type checker -- would mean maintaining two sources of truth that inevitably drift apart. For a language where the transpiler <em>is</em> the type checker, piggybacking on its results isn't coupling; it's architectural honesty.</p>
<p>The bigger limitation is that <code>lspVarTypes</code> only captures variables. Expression types (what's the type of <code>list.Map(f).Filter(g)</code>?) aren't in the map; those get resolved on the fly by <code>typeAtDot</code> walking the chain and consulting the <code>RichAST</code>'s method signatures. That's a story for another post.</p>
<hr />
<p><em>Next in the series: how dot-completion resolves method chains when the type of each step depends on the previous one.</em></p>
]]></content:encoded></item><item><title><![CDATA[22x Faster Builds: Inside GALA's Compilation Performance Journey]]></title><description><![CDATA[In GALA 0.24.0, we reduced compilation time for multi-file packages from 44.7 seconds to 2.7 seconds -- a 22x improvement. This post covers what was slow, how we found it, and what we did about it.
The Problem
GALA transpiles .gala files to .go files...]]></description><link>https://martianov.dev/22x-faster-builds-gala-compilation-performance</link><guid isPermaLink="true">https://martianov.dev/22x-faster-builds-gala-compilation-performance</guid><category><![CDATA[compiler]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[optimization]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 29 Mar 2026 22:07:59 GMT</pubDate><content:encoded><![CDATA[<p>In GALA 0.24.0, we reduced compilation time for multi-file packages from 44.7 seconds to 2.7 seconds -- a 22x improvement. This post covers what was slow, how we found it, and what we did about it.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>GALA transpiles <code>.gala</code> files to <code>.go</code> files. For single-file packages, this is fast -- parse, analyze, transform, emit. But real projects have multi-file packages. A server might have 7 files in the same package, each file needing to know about types, methods, and sealed types defined in the other 6.</p>
<p>Before 0.24.0, each file in a package was transpiled as a separate process invocation. The <code>gala transpile</code> command was called once per file, and each invocation:</p>
<ol>
<li>Parsed the target file</li>
<li>Scanned all sibling files to extract type information</li>
<li>Analyzed the full dependency graph (imports, Go type inference, sealed type metadata)</li>
<li>Transformed the AST to Go</li>
<li>Emitted the output</li>
</ol>
<p>For a 7-file package, step 2 and 3 happened 7 times, each time re-parsing the same siblings and re-resolving the same imports. The Go type inference step -- which shells out to <code>go list</code> to resolve return types and method signatures from Go packages -- was particularly expensive, adding hundreds of milliseconds per invocation.</p>
<p>The gala-server package (7 files) took 44.7 seconds. Most of that was redundant work.</p>
<h2 id="heading-profiling-first">Profiling First</h2>
<p>GALA 0.24.0 introduced <code>GALA_PROFILE=1</code>, an environment variable that enables compilation profiling. When set, the transpiler emits timing breakdowns for every phase:</p>
<pre><code>[PROFILE] Parse:     <span class="hljs-number">12</span>ms
[PROFILE] Analyze:   <span class="hljs-number">340</span>ms
[PROFILE] Transform: <span class="hljs-number">85</span>ms
[PROFILE] Emit:      <span class="hljs-number">3</span>ms
[PROFILE] Total:     <span class="hljs-number">440</span>ms
</code></pre><p>Running this across all 7 files revealed the pattern immediately: analysis dominated. Each file spent ~340ms in analysis, and the analysis for each file was doing nearly identical work -- re-parsing sibling files, re-resolving Go types, re-building sealed type metadata.</p>
<p>The profiling data made the optimization path obvious. Without it, we might have guessed wrong and optimized the transform phase (which was already fast) or the parser (which was negligible).</p>
<p><strong>Lesson: always profile before optimizing.</strong> The GALA project enforces this as policy -- the memory file literally says "always profile with GALA_PROFILE=1 before optimizing."</p>
<h2 id="heading-solution-1-batchanalyzer">Solution 1: BatchAnalyzer</h2>
<p>The first optimization was architectural: share analysis state across files in the same package.</p>
<p>The <code>BatchAnalyzer</code> takes all files in a package at once. It parses each file, then runs a single unified analysis pass that:</p>
<ul>
<li>Extracts type declarations, method signatures, and sealed type definitions from all files</li>
<li>Runs Go type inference once for the entire package's import set</li>
<li>Builds a shared metadata cache that every file can read from</li>
</ul>
<p>Instead of 7 analysis passes (one per file), there is now 1 analysis pass for the whole package. The analysis result is then distributed to each file's transformer.</p>
<p>The implementation required refactoring the analyzer's entry point. Previously, <code>NewGalaAnalyzer</code> accepted a single file and optional sibling file paths. The new <code>NewBatchAnalyzer</code> accepts all files upfront and returns a shared analysis context:</p>
<pre><code>Before:  <span class="hljs-number">7</span> files x <span class="hljs-number">1</span> analyzer each  = <span class="hljs-number">7</span> full analyses
<span class="hljs-attr">After</span>:   <span class="hljs-number">7</span> files x <span class="hljs-number">1</span> shared analyzer = <span class="hljs-number">1</span> full analysis + <span class="hljs-number">7</span> lookups
</code></pre><h2 id="heading-solution-2-disk-cache">Solution 2: Disk Cache</h2>
<p>Even with batch analysis, the Go type inference step (resolving return types, struct fields, and method signatures from Go standard library and third-party packages) was still expensive for the first compilation of a package.</p>
<p>GALA 0.24.0 introduced a disk cache at <code>.gala/cache/</code>. The cache stores analysis results keyed by content hash -- a SHA-256 of the file contents plus the contents of all its imports. If the file and its dependencies haven't changed, the cached analysis is reused.</p>
<p>The cache format is simple: JSON files named by their content hash. On a warm cache, the analysis phase drops from ~340ms to ~5ms per file, because no Go type inference or sibling parsing is needed.</p>
<p>Cache invalidation is content-addressed, so it is always correct: if any source file changes, its hash changes, and the cache misses. No timestamps, no file watchers, no stale cache bugs.</p>
<h2 id="heading-solution-3-batch-transpilation">Solution 3: Batch Transpilation</h2>
<p>The final piece was eliminating the process-per-file overhead. Before 0.24.0, the build system (Bazel or <code>gala build</code>) invoked the transpiler binary once per <code>.gala</code> file. Each invocation paid the cost of process startup, Go runtime initialization, and flag parsing.</p>
<p>Batch transpilation runs a single transpiler process for all files in a package. The process:</p>
<ol>
<li>Parses all input files</li>
<li>Runs the BatchAnalyzer (one shared analysis pass)</li>
<li>Transforms each file using the shared analysis context</li>
<li>Emits all <code>.go</code> files</li>
</ol>
<p>This eliminated 6 process startups (for a 7-file package) and enabled the shared analysis to happen in-memory rather than being serialized to disk and re-read.</p>
<h2 id="heading-results">Results</h2>
<p>Benchmarked on the gala-server package (7 <code>.gala</code> files):</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Configuration</td><td>Time</td><td>Speedup</td></tr>
</thead>
<tbody>
<tr>
<td>0.23.x (file-at-a-time, no cache)</td><td>44.7s</td><td>baseline</td></tr>
<tr>
<td>Batch analysis, no cache</td><td>6.1s</td><td>7.3x</td></tr>
<tr>
<td>Batch analysis + disk cache (cold)</td><td>5.8s</td><td>7.7x</td></tr>
<tr>
<td>Batch analysis + disk cache (warm)</td><td>2.7s</td><td>16.6x</td></tr>
<tr>
<td>Batch transpilation + warm cache</td><td><strong>2.0s</strong></td><td><strong>22x</strong></td></tr>
</tbody>
</table>
</div><p>The warm-cache number is the steady-state experience during development: edit a file, rebuild, and the unchanged files hit the cache while only the modified file gets re-analyzed.</p>
<p>For single-file packages (like most examples), compilation time is unchanged at ~200-400ms. The optimization specifically targets the multi-file case where redundant work was the bottleneck.</p>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<p><strong>1. Profile before optimizing.</strong> The profiling data pointed directly at analysis as the bottleneck. Without it, we might have spent time optimizing the wrong phase.</p>
<p><strong>2. Shared state beats repeated computation.</strong> The batch analyzer is conceptually simple -- do the work once, share the result -- but it required restructuring the analyzer's API from "single file in, analysis out" to "all files in, shared context out."</p>
<p><strong>3. Content-addressed caching is worth the investment.</strong> It is simpler than timestamp-based caching (no clock skew issues, no "did the file actually change?" ambiguity) and it is always correct. The overhead of computing SHA-256 hashes is negligible compared to the analysis it replaces.</p>
<p><strong>4. Process startup costs add up.</strong> For a language tool that gets invoked hundreds of times during a build, the cost of spawning a new process each time is real. Batch mode amortizes this to one process per package.</p>
<p><strong>5. The optimization was straightforward once the data was clear.</strong> There was no clever algorithm, no sophisticated caching strategy. The 22x improvement came from eliminating obviously redundant work that the profiler made visible.</p>
<h2 id="heading-try-it">Try It</h2>
<p>GALA 0.24.0+ is available now. To see compilation timing for your own packages:</p>
<pre><code class="lang-bash">GALA_PROFILE=1 gala build ./...
</code></pre>
<p>The playground at https://gala-playground.fly.dev transpiles instantly (single-file), but for multi-file projects, the batch transpilation makes a meaningful difference in the edit-compile-run cycle.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Reliable Transpiler: Lessons from 80+ Bug Fixes]]></title><description><![CDATA[GALA has shipped 46 releases in two months. Along the way, we have fixed over 80 bugs in type inference, code generation, cross-package resolution, and build tooling. This post is an honest account of what went wrong, how we caught it, and what we le...]]></description><link>https://martianov.dev/building-reliable-transpiler-80-bug-fixes</link><guid isPermaLink="true">https://martianov.dev/building-reliable-transpiler-80-bug-fixes</guid><category><![CDATA[compiler]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 29 Mar 2026 22:07:59 GMT</pubDate><content:encoded><![CDATA[<p>GALA has shipped 46 releases in two months. Along the way, we have fixed over 80 bugs in type inference, code generation, cross-package resolution, and build tooling. This post is an honest account of what went wrong, how we caught it, and what we learned about building a transpiler that people can actually trust.</p>
<h2 id="heading-the-challenge">The Challenge</h2>
<p>Transpilers occupy an unusual spot in the compiler landscape. A traditional compiler owns the entire pipeline from source to machine code. A transpiler maps one language's semantics onto another language's type system, and any mismatch between the two becomes a bug.</p>
<p>GALA maps functional concepts -- sealed types, pattern matching, immutability, monadic types -- onto Go's type system. Go has no sum types, no pattern matching, no <code>Option[T]</code>. Every one of these features must be lowered into valid Go code that the Go compiler accepts. The type inference engine must reason about GALA's types (sealed variants, generic monads, lambda parameter inference) while also querying Go's type system (return types of <code>os.ReadFile</code>, field types of <code>http.Request</code>).</p>
<p>This two-language gap is where most of our bugs lived.</p>
<h2 id="heading-categories-of-bugs">Categories of Bugs</h2>
<h3 id="heading-type-inference-35-fixes">Type Inference (35+ fixes)</h3>
<p>Type inference bugs were the most common category. GALA's type system is richer than Go's -- it has sealed types, generic type parameter inference, lambda parameter inference from context, and Hindley-Milner-style unification for complex generic chains.</p>
<p><strong>Chained method calls</strong> were a recurring source of issues. Consider:</p>
<pre><code class="lang-gala">val result = Some(42).Map((x) =&gt; x * 2).GetOrElse(0)
</code></pre>
<p>The transpiler must:</p>
<ol>
<li>Infer that <code>Some(42)</code> produces <code>Option[int]</code></li>
<li>Resolve <code>Map</code> on <code>Option[int]</code> with a <code>func(int) int</code> lambda</li>
<li>Infer the lambda parameter <code>x</code> as <code>int</code> from the method signature</li>
<li>Determine that <code>Map</code> returns <code>Option[int]</code></li>
<li>Resolve <code>GetOrElse</code> on <code>Option[int]</code> returning <code>int</code></li>
</ol>
<p>If any step fails, the chain breaks. Early versions would lose the type at step 4, causing <code>GetOrElse</code> to resolve against <code>Option[any]</code> and generate incorrect Go code.</p>
<p><strong>Void lambdas</strong> were another painful area. Go functions that take <code>func(T)</code> callbacks (no return value) need different treatment than <code>func(T) U</code> callbacks. The transpiler must detect when a lambda is expected to return nothing and avoid wrapping its body in a return statement. Getting this wrong produced Go code that failed to compile with "too many return values."</p>
<p><strong>Block lambda return types</strong> required special handling. When a lambda has a block body:</p>
<pre><code class="lang-gala">val result = list.Map((x) =&gt; {
    val doubled = x * 2
    return doubled
})
</code></pre>
<p>The return type must be inferred from the last expression or explicit <code>return</code> statement inside the block. This interacts with generic type parameter substitution -- if <code>Map</code> is <code>Map[U](f func(T) U) Array[U]</code>, the transpiler must unify <code>U</code> with the block's return type.</p>
<h3 id="heading-code-generation-20-fixes">Code Generation (20+ fixes)</h3>
<p>Code generation bugs produced syntactically valid but semantically wrong Go code.</p>
<p><strong>Unused imports</strong> were a constant annoyance. GALA auto-imports the <code>std</code> package for <code>Option</code>, <code>Try</code>, etc. But if a file uses only <code>Println</code> (which maps to <code>fmt.Println</code>), the transpiler might emit an <code>import "martianoff/gala/std"</code> that nothing references. The Go compiler rejects unused imports, so the generated code would not compile.</p>
<p>The fix was an <code>ImportManager</code> that tracks which imports are actually referenced during code generation and only emits those. This sounds simple, but the original implementation had 4 separate import tracking subsystems that had grown organically -- one for explicit user imports, one for auto-imports, one for sealed type companions, and one for Go interop helpers. Unifying these into a single <code>ImportManager</code> was one of the larger refactoring efforts (covered below).</p>
<p><strong>IIFE wrapping</strong> for if-expressions and match-expressions required careful handling. GALA's <code>if</code> and <code>match</code> are expressions that produce values, but Go's <code>if</code> and <code>switch</code> are statements. The transpiler wraps them in immediately-invoked function expressions:</p>
<pre><code class="lang-go"><span class="hljs-comment">// GALA: val x = if (cond) "yes" else "no"</span>
<span class="hljs-comment">// Go:</span>
x := <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">if</span> cond {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"yes"</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-string">"no"</span>
}()
</code></pre>
<p>Getting the return type of these IIFEs wrong -- especially when the branches had different levels of type specificity -- caused type mismatches in the generated Go.</p>
<h3 id="heading-cross-package-resolution-15-fixes">Cross-Package Resolution (15+ fixes)</h3>
<p>Multi-file packages and cross-package imports introduced a class of bugs around name resolution.</p>
<p><strong>Dot imports</strong> (<code>. "package/path"</code>) bring all exported names into scope. The transpiler must know which names came from dot imports versus local definitions, because the generated Go code needs explicit package prefixes for non-dot imports. Early versions would sometimes lose track of a name's origin, generating <code>json.Codec</code> when the user had <code>import . "martianoff/gala/json"</code> (which should produce just <code>Codec</code>).</p>
<p><strong>Sibling file scanning</strong> -- extracting type information from other files in the same package -- had to be carefully scoped. The scanner must extract type declarations, method signatures, and sealed type definitions from siblings, but it must NOT scan files from <code>main</code> or <code>test</code> packages when analyzing a library package. A bug where sibling scanning included <code>main</code> package files caused type corruption in library packages.</p>
<h3 id="heading-build-system-10-fixes">Build System (10+ fixes)</h3>
<p>Bazel-specific bugs were their own category.</p>
<p><strong>Symlinks and junctions</strong> on Windows caused issues with Bazel's sandboxing. GALA genrules need filesystem access to the Go SDK for type inference, requiring <code>tags = ["no-sandbox"]</code>. But Bazel still creates symlink trees for inputs, and Windows junction handling in Go's <code>filepath.Walk</code> behaved differently than on Linux.</p>
<p><strong>Cache races</strong> in Bazel's parallel execution could cause two genrules to write the same cache file simultaneously. The fix was content-addressed caching with atomic writes (write to a temp file, then rename).</p>
<h2 id="heading-the-testing-infrastructure">The Testing Infrastructure</h2>
<p>Catching these bugs before users hit them required multiple layers of testing.</p>
<h3 id="heading-compilation-gate-test">Compilation Gate Test</h3>
<p>Every example in the <code>examples/</code> directory (216 files as of today) has a corresponding expected-output file. The compilation gate test transpiles every example, compiles the generated Go, runs it, and compares the output. If any example fails to compile or produces wrong output, the gate fails.</p>
<p>This is the single most valuable test. It catches regressions that unit tests miss because unit tests test individual phases, while the gate test exercises the full pipeline end-to-end.</p>
<h3 id="heading-type-inference-regression-suite">Type Inference Regression Suite</h3>
<p>A dedicated test suite with 14+ cases targeting specific type inference scenarios that have broken in the past:</p>
<ul>
<li>Generic method chains with lambda parameter inference</li>
<li>Sealed type pattern matching with nested extractors</li>
<li>FoldLeft accumulator type inference from zero value</li>
<li>Block lambda return type propagation through generic boundaries</li>
<li>Void lambda detection for Go callback functions</li>
</ul>
<p>Each case is a minimal <code>.gala</code> file that exercises one inference path. When a type inference bug is fixed, a regression case is added to prevent it from recurring.</p>
<h3 id="heading-metadata-validation-pass">Metadata Validation Pass</h3>
<p>Setting <code>GALA_VALIDATE_METADATA=1</code> enables a post-analysis validation pass that checks:</p>
<ul>
<li>All type references resolve to concrete types (no leftover <code>NilType</code> or <code>any</code> placeholders)</li>
<li>All method calls resolve to known methods on the resolved type</li>
<li>Sealed type variants have correct discriminator values</li>
<li>Generic type parameters are fully substituted</li>
</ul>
<p>This catches bugs where the analyzer produces metadata that looks plausible but is subtly wrong -- the kind of bug that only manifests as incorrect Go output or a confusing Go compiler error.</p>
<h3 id="heading-battle-test-workflow">Battle Test Workflow</h3>
<p>A CI workflow that builds and runs all examples, the standard library tests, and several multi-file showcase projects. It runs on both Linux and Windows to catch platform-specific issues (Windows path separators, symlink handling, GOROOT propagation).</p>
<h2 id="heading-bug-stories">Bug Stories</h2>
<h3 id="heading-void-lambda-error-swallowing">Void Lambda Error Swallowing</h3>
<p>In version 0.22.0, we discovered that Go functions accepting <code>func(T)</code> callbacks could silently swallow errors. Consider:</p>
<pre><code class="lang-gala">val items = SliceOf("file1.txt", "file2.txt")
items.ForEach((name) =&gt; os.Remove(name))
</code></pre>
<p><code>os.Remove</code> returns an <code>error</code>, but <code>ForEach</code> takes <code>func(string)</code> -- a void callback. The transpiler was happily compiling this, discarding the error return. In Go, ignoring an error return is at least visible in the code (<code>_ = os.Remove(name)</code>). In GALA, the void lambda made it invisible.</p>
<p>The fix had two parts. First, the transpiler now rejects void lambdas where the body expression returns an error type, producing a compile error: "void lambda discards error return from os.Remove; use Try or handle the error explicitly." Second, we added <code>FromError(error) Try[Void]</code> to the standard library, providing a clean way to convert Go error returns into GALA's type-safe error handling:</p>
<pre><code class="lang-gala">items.ForEach((name) =&gt; {
    FromError(os.Remove(name)).OnFailure((e) =&gt; {
        Println(s"Failed to remove $name: ${e.Error()}")
    })
})
</code></pre>
<p>This is a case where a bug fix led to a language feature. The <code>Void</code> type and <code>FromError</code> constructor exist because a real interop edge case demanded them.</p>
<h3 id="heading-importmanager-unification">ImportManager Unification</h3>
<p>By version 0.22.0, the transpiler had accumulated four separate import tracking systems:</p>
<ol>
<li><strong>User imports</strong> -- explicit <code>import</code> declarations in the source file</li>
<li><strong>Auto-imports</strong> -- automatically added for <code>std</code>, <code>fmt</code>, etc.</li>
<li><strong>Sealed type imports</strong> -- added when pattern matching references companion objects from other packages</li>
<li><strong>Go interop imports</strong> -- added when the transpiler generates calls to Go standard library functions</li>
</ol>
<p>Each system maintained its own state, its own deduplication logic, and its own output formatting. Bugs would appear when two systems disagreed: the auto-import system would add <code>"fmt"</code> and then the user import system would also add <code>"fmt"</code>, producing a duplicate import that Go rejects.</p>
<p>The unification in 0.23.0 replaced all four with a single <code>ImportManager</code> that:</p>
<ul>
<li>Accepts imports from any source (user, auto, sealed, interop)</li>
<li>Deduplicates by import path</li>
<li>Tracks actual usage during code generation</li>
<li>Emits only imports that are referenced in the output</li>
</ul>
<p>The refactoring touched 12 files and required updating every code path that previously used one of the four systems. The result was a net reduction of ~400 lines of code and elimination of an entire class of "duplicate import" and "unused import" bugs.</p>
<h3 id="heading-the-22x-performance-gain">The 22x Performance Gain</h3>
<p>The compilation performance story (covered in detail in the companion post) is also a reliability story. The file-at-a-time transpilation model was not just slow -- it was fragile. Each file's analysis was an independent computation, and if two files in the same package inferred slightly different metadata for a shared type (due to different analysis orderings or cache states), the generated Go files could be inconsistent.</p>
<p>Batch transpilation eliminated this class of bugs entirely by ensuring all files in a package see the same analysis result.</p>
<h2 id="heading-what-makes-transpiler-bugs-unique">What Makes Transpiler Bugs Unique</h2>
<p>Transpiler bugs differ from traditional compiler bugs in one important way: the error message comes from the wrong compiler.</p>
<p>When GALA generates incorrect Go code, the error is reported by the Go compiler -- not GALA. The user sees a Go error pointing at generated code they did not write. The mental model required to debug this ("GALA generated wrong Go, which means my GALA source triggered an edge case in the type inference engine") is harder than debugging a direct compiler error.</p>
<p>This is why GALA invests heavily in catching bugs before code generation. The metadata validation pass, the type inference regression suite, and the compilation gate test all exist to ensure that if something goes wrong, it fails in the GALA transpiler with a GALA-level error message, not in the Go compiler with a cryptic message about generated code.</p>
<h2 id="heading-current-state">Current State</h2>
<p>As of version 0.25.2:</p>
<ul>
<li><strong>216 verification examples</strong> compile and produce correct output</li>
<li><strong>14 type inference regression cases</strong> guard against known failure modes</li>
<li><strong>Full CI on Linux and Windows</strong> catches platform-specific issues</li>
<li><strong>Metadata validation</strong> available for development builds</li>
<li><strong>Zero known open bugs</strong> in the type inference engine (though new ones will certainly appear as usage grows)</li>
</ul>
<p>The transpiler is not done. Complex generic type inference across package boundaries still has rough edges. The interaction between Go's type system and GALA's sealed types occasionally produces surprising results. But the testing infrastructure means that when bugs appear, they are caught quickly, fixed with a regression test, and stay fixed.</p>
<h2 id="heading-try-it">Try It</h2>
<p>The GALA playground is at https://gala-playground.fly.dev. For local development:</p>
<pre><code class="lang-bash">gala build ./...
gala <span class="hljs-built_in">test</span> ./...
</code></pre>
<p>If you find a bug, the best report is a minimal <code>.gala</code> file that fails to compile or produces wrong output. That becomes a regression test, and regression tests are how transpilers get reliable.</p>
]]></content:encoded></item><item><title><![CDATA[The State of GALA: March 2026]]></title><description><![CDATA[What Is GALA?
GALA (Go Alternative Language) is a programming language that transpiles to Go. It takes Go's runtime performance, static typing, and deployment simplicity, and adds the features that Go developers reach for repeatedly but never get: se...]]></description><link>https://martianov.dev/state-of-gala-march-2026</link><guid isPermaLink="true">https://martianov.dev/state-of-gala-march-2026</guid><category><![CDATA[compiler]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[programming languages]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 29 Mar 2026 22:04:45 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-what-is-gala">What Is GALA?</h2>
<p>GALA (Go Alternative Language) is a programming language that transpiles to Go. It takes Go's runtime performance, static typing, and deployment simplicity, and adds the features that Go developers reach for repeatedly but never get: sealed types with exhaustive pattern matching, immutability by default, monadic error handling, functional collections, and type inference that actually works across generic boundaries.</p>
<p>GALA is not a new runtime or a managed language. Every <code>.gala</code> file becomes a <code>.go</code> file. Your existing Go libraries, tools, and deployment pipelines work unchanged. The goal is simple: write safer, more expressive code that compiles to the same fast binaries you already rely on.</p>
<p>Two months ago, GALA shipped version 0.0.1 -- a transpiler that could handle basic structs, <code>val</code>/<code>var</code> declarations, and simple pattern matching. Today, version 0.25.2 delivers a language with a rich standard library, a zero-reflection JSON codec, async primitives, and a compilation pipeline that processes multi-file packages in under 3 seconds.</p>
<h2 id="heading-by-the-numbers">By the Numbers</h2>
<p>Since the first release on January 23, 2026:</p>
<ul>
<li><strong>46 releases</strong> shipped (roughly one every 1.5 days)</li>
<li><strong>216 verification examples</strong> in the test suite</li>
<li><strong>80+ bug fixes</strong> across type inference, code generation, and build tooling</li>
<li><strong>19 standard library modules</strong> (std, collections, concurrent, json, regex, io, go_interop)</li>
<li><strong>22x compilation speedup</strong> for multi-file packages (44.7s down to 2.7s)</li>
</ul>
<p>This pace was only possible because GALA's transpilation architecture allows rapid iteration: change the transformer, run the examples, see real Go output. No bootstrapping a VM, no managing a garbage collector.</p>
<h2 id="heading-highlight-reel">Highlight Reel</h2>
<h3 id="heading-sealed-types-and-exhaustive-pattern-matching">Sealed Types and Exhaustive Pattern Matching</h3>
<p>Sealed types are GALA's answer to Go's lack of sum types. They define algebraic data types concisely, with the transpiler generating all the boilerplate -- parent structs, companion objects, <code>Apply</code>/<code>Unapply</code> methods, and discriminator checks.</p>
<pre><code class="lang-gala">sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
    case Point()
}

val area = shape match {
    case Circle(r)      =&gt; 3.14159 * r * r
    case Rectangle(w, h) =&gt; w * h
    case Point()         =&gt; 0.0
    // No default needed -- compiler verifies exhaustiveness
}
</code></pre>
<p>The standard library's <code>Option[T]</code>, <code>Either[A, B]</code>, and <code>Try[T]</code> are all sealed types. They use the same resolution mechanisms as user-defined types -- no special-casing in the transpiler.</p>
<h3 id="heading-zero-reflection-json-codec">Zero-Reflection JSON Codec</h3>
<p>GALA's JSON codec is built on <code>StructMeta[T]</code>, a compiler intrinsic that provides type-safe struct introspection without any reflection at runtime. The codec uses a builder pattern with naming strategies:</p>
<pre><code class="lang-gala">import . "martianoff/gala/json"

struct Person(FirstName string, LastName string, Age int)

val codec = Codec[Person](SnakeCase())
    .Omit("Password")
    .Rename("Email", "email_address")

val jsonStr = codec.Encode(person).Get()
// =&gt; {"first_name":"Alice","last_name":"Smith","age":30}

// Pattern matching on JSON strings
val result = jsonStr match {
    case codec(p) =&gt; s"Found: ${p.FirstName}, age ${p.Age}"
    case _ =&gt; "invalid JSON"
}
</code></pre>
<p>No struct tags. No <code>encoding/json</code> at runtime. The transpiler generates specialized, typed serialization code.</p>
<h3 id="heading-functional-collections">Functional Collections</h3>
<p>GALA ships immutable <code>Array</code>, <code>List</code>, <code>HashMap</code>, <code>TreeMap</code>, <code>HashSet</code>, and <code>TreeSet</code> with full functional APIs:</p>
<pre><code class="lang-gala">import . "martianoff/gala/collection_immutable"

val nums = ArrayOf(1, 2, 3, 4, 5)
val doubled = nums.Map((x) =&gt; x * 2)
val sum = nums.FoldLeft(0, (acc, x) =&gt; acc + x)
val evens = nums.Filter((x) =&gt; x % 2 == 0)

// Collect: filter + transform in one pass
val evenDoubled = nums.Collect({ case n if n % 2 == 0 =&gt; n * 2 })
</code></pre>
<p>Sequence pattern matching works on any collection implementing the <code>Seq</code> interface:</p>
<pre><code class="lang-gala">val result = list match {
    case List(head, tail...) =&gt; s"First: $head, rest: ${tail.Size()}"
    case _ =&gt; "empty"
}
</code></pre>
<h3 id="heading-monadic-error-handling-try-either-future">Monadic Error Handling: Try, Either, Future</h3>
<p><code>Try[T]</code> catches panics and wraps them as <code>Failure</code>, enabling railway-oriented programming without <code>if err != nil</code> chains:</p>
<pre><code class="lang-gala">val result = Try(() =&gt; riskyOperation())
    .Map((v) =&gt; v * 2)
    .Recover((e) =&gt; 0)

val msg = result match {
    case Success(v) =&gt; s"Got: $v"
    case Failure(e) =&gt; s"Error: ${e.Error()}"
}
</code></pre>
<p><code>Future[T]</code> provides async computation with familiar monadic operations:</p>
<pre><code class="lang-gala">import . "martianoff/gala/concurrent"

val async = FutureApply[int](() =&gt; expensiveComputation())
val doubled = async.Map((v) =&gt; v * 2)
val value = doubled.Await().GetOrElse(0)
</code></pre>
<h3 id="heading-default-parameters-and-named-arguments">Default Parameters and Named Arguments</h3>
<p>Functions support default values and named arguments -- the compiler reorders and injects them at the call site:</p>
<pre><code class="lang-gala">func connect(host string, port int = 8080, tls bool = true) Connection {
    // ...
}

connect("localhost")                // port=8080, tls=true
connect("localhost", tls = false)   // port=8080, tls=false
</code></pre>
<p>Named arguments also work with struct construction and <code>Copy()</code> method overrides.</p>
<h3 id="heading-string-interpolation">String Interpolation</h3>
<p>Two forms: <code>s"..."</code> for standard interpolation and <code>f"..."</code> for explicit format control:</p>
<pre><code class="lang-gala">val name = "world"
val pi = 3.14159
Println(s"Hello $name")        // Hello world
Println(f"$pi%.2f")            // 3.14
</code></pre>
<p>Auto-detected format verbs: <code>%s</code> for strings, <code>%d</code> for ints, <code>%g</code> for floats, <code>%t</code> for bools. No import needed for <code>Println</code> or <code>Print</code>.</p>
<h3 id="heading-22x-faster-compilation">22x Faster Compilation</h3>
<p>Version 0.24.0 introduced batch transpilation, bringing multi-file package compilation from 44.7 seconds down to 2.7 seconds. The key insight: each file in a package was re-analyzing the entire dependency graph from scratch. Sharing analysis state across files in the same package eliminated the redundancy. See the companion blog post for the full story.</p>
<h3 id="heading-retry-combinator">Retry Combinator</h3>
<p>Version 0.25.0 added a retry combinator with pluggable backoff strategies:</p>
<pre><code class="lang-gala">import . "martianoff/gala/concurrent"

val result = Retry(
    maxRetries = 3,
    backoff = ExponentialBackoff(100),
    fn = () =&gt; fetchFromAPI(),
)
</code></pre>
<p>Three strategies ship out of the box: <code>ConstantBackoff</code>, <code>ExponentialBackoff</code>, and <code>NoBackoff</code>.</p>
<h2 id="heading-the-standard-library-today">The Standard Library Today</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Package</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><code>std</code></td><td>Option, Either, Try, Tuple, StructMeta, Immutable</td></tr>
<tr>
<td><code>collection_immutable</code></td><td>Array, List, HashMap, TreeMap, HashSet, TreeSet</td></tr>
<tr>
<td><code>concurrent</code></td><td>Future, Promise, ExecutionContext, Retry</td></tr>
<tr>
<td><code>json</code></td><td>Zero-reflection codec with builder pattern</td></tr>
<tr>
<td><code>regex</code></td><td>Pattern matching extractors for regular expressions</td></tr>
<tr>
<td><code>io</code></td><td>Lazy IO effect type with composition</td></tr>
<tr>
<td><code>go_interop</code></td><td>SliceOf, MapOf, nil-safe helpers for Go interop</td></tr>
</tbody>
</table>
</div><p>Every standard library module follows the same rules as user code. There are no special cases in the transpiler for <code>std</code> types -- if you can build <code>Option[T]</code>, you can build your own monads the same way.</p>
<h2 id="heading-showcase-projects">Showcase Projects</h2>
<p>Several non-trivial projects have been built with GALA during development:</p>
<ul>
<li><strong>GALA Playground</strong> (https://gala-playground.fly.dev) -- a web-based editor with live transpilation, deployed on Fly.io</li>
<li><strong>gala-server</strong> -- a 7-file HTTP server package that served as the real-world benchmark for compilation performance</li>
<li><strong>State Machine</strong> -- demonstrating sealed types as state representations with exhaustive transition matching</li>
<li><strong>Log Analyzer</strong> -- a functional pipeline using <code>Try</code>, regex pattern matching, and collection operations</li>
</ul>
<h2 id="heading-whats-next">What's Next</h2>
<p>GALA is still young. The transpiler handles a broad set of Go interop scenarios, but there are rough edges -- particularly around complex generic type inference across package boundaries. The near-term focus is:</p>
<ol>
<li><strong>Stability</strong> -- expanding the type inference regression suite beyond its current 14 cases</li>
<li><strong>Cross-package generics</strong> -- improving type resolution for external GALA modules</li>
<li><strong>IDE support</strong> -- GoLand plugin for GALA syntax highlighting and navigation</li>
<li><strong>Package registry</strong> -- making it easy to publish and consume GALA libraries</li>
</ol>
<p>If you write Go and have wished for pattern matching, immutability, or proper sum types, give GALA a try. The playground is at https://gala-playground.fly.dev, and the full language specification is in the repository.</p>
<p>The transpiler is honest about what it is: a source-to-source compiler that generates readable Go. When something goes wrong, you can always read the output. That transparency is a feature, not a limitation.</p>
]]></content:encoded></item><item><title><![CDATA[samber/lo Has 21K Stars. Here's What It Would Look Like as a Language Feature.]]></title><description><![CDATA[samber/lo has 21,000 stars. samber/mo has 3,300. Together, they represent one of the loudest feature requests the Go community has ever filed -- not through proposals or GitHub issues, but through ado]]></description><link>https://martianov.dev/samber-lo-has-21k-stars-here-s-what-it-would-look-like-as-a-language-feature</link><guid isPermaLink="true">https://martianov.dev/samber-lo-has-21k-stars-here-s-what-it-would-look-like-as-a-language-feature</guid><category><![CDATA[gala]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[golang]]></category><category><![CDATA[Functional Programming]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Thu, 19 Mar 2026 03:40:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64d53a91055f78c80c13d0f6/e71b8192-0690-4fe2-96ea-f6e2787698da.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>samber/lo has 21,000 stars. samber/mo has 3,300. Together, they represent one of the loudest feature requests the Go community has ever filed -- not through proposals or GitHub issues, but through adoption. Tens of thousands of Go developers have looked at <code>for i, v := range slice</code> and said: there has to be a better way.</p>
<p>They are right. And lo and mo are genuinely good solutions within the constraints of Go's type system. But those constraints are real, and they shape every API in ways that become visible the moment you compare them to what a language-level implementation looks like.</p>
<p>I am the author of <a href="https://github.com/martianoff/gala">GALA</a>, a language that transpiles to Go. This is not a "lo is bad" post. This is a post about what happens when you move functional patterns from library space into language space -- and what you gain and lose in the process.</p>
<h2>What lo and mo solve</h2>
<p>lo gives Go developers Lodash-style collection operations: Map, Filter, Reduce, GroupBy, Chunk, Flatten, and dozens more. Before generics landed in Go 1.18, this was impossible without code generation or <code>interface{}</code>. lo was one of the first libraries to prove that Go generics could deliver real ergonomic gains.</p>
<p>mo goes further. It brings monadic types to Go: <code>Option[T]</code>, <code>Result[T]</code>, <code>Either[L, R]</code>, <code>Future[T]</code>. These are types that encode success/failure, presence/absence, and either/or semantics directly in the type system, enabling method chaining instead of <code>if err != nil</code> waterfalls.</p>
<p>Both libraries are well-designed. Both solve real problems. But both also bump into limitations that no Go library can work around.</p>
<h2>Side by side: Collection operations</h2>
<p>Here is a common pattern -- filter a list of users, extract names, aggregate a result.</p>
<p><strong>samber/lo:</strong></p>
<pre><code class="language-go">evens := lo.Filter(users, func(u User, _ int) bool {
    return u.Active &amp;&amp; u.Age &gt;= 18
})

names := lo.Map(evens, func(u User, _ int) string {
    return u.Name
})

result := strings.Join(names, ", ")

totalAge := lo.Reduce(users, func(acc int, u User, _ int) int {
    return acc + u.Age
}, 0)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">val result = users
    .Filter((u) =&gt; u.Active &amp;&amp; u.Age &gt;= 18)
    .Map((u) =&gt; u.Name)
    .MkString(", ")

val totalAge = users.FoldLeft(0, (acc, u) =&gt; acc + u.Age)
</code></pre>
<p>Three differences stand out.</p>
<p><strong>The unused index parameter.</strong> Every lo callback takes an extra <code>_ int</code> index parameter, even when you do not need it. This is a design compromise -- lo provides a single API for both indexed and non-indexed access. In GALA, lambdas take only the parameters the operation requires.</p>
<p><strong>Method chaining vs function wrapping.</strong> lo uses free functions: <code>lo.Filter(lo.Map(xs, f), g)</code>. To chain operations, you nest calls or use intermediate variables. GALA collections are types with methods, so operations chain naturally: <code>xs.Map(f).Filter(g)</code>. The pipeline reads left to right, top to bottom.</p>
<p><strong>Lambda syntax.</strong> Go requires <code>func</code>, explicit parameter types, explicit return type, and curly braces. GALA infers parameter types from the method signature: <code>(u) =&gt; u.Name</code> instead of <code>func(u User, _ int) string { return u.Name }</code>. The type information is still checked -- you just do not have to write it.</p>
<p>Here is a numeric example to make the contrast sharper:</p>
<p><strong>samber/lo:</strong></p>
<pre><code class="language-go">evens := lo.Filter(nums, func(x int, _ int) bool {
    return x%2 == 0
})
squares := lo.Map(evens, func(x int, _ int) int {
    return x * x
})
sum := lo.Reduce(squares, func(acc int, x int, _ int) int {
    return acc + x
}, 0)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">val result = nums
    .Filter((x) =&gt; x % 2 == 0)
    .Map((x) =&gt; x * x)
    .FoldLeft(0, (acc, x) =&gt; acc + x)
</code></pre>
<p>Same logic. One reads as a pipeline. The other reads as a sequence of transformations with ceremony around each step.</p>
<h2>Side by side: Option types</h2>
<p>mo brings <code>Option[T]</code> to Go, which is a genuine improvement over nil pointers.</p>
<p><strong>samber/mo:</strong></p>
<pre><code class="language-go">opt := mo.Some(42)
result := opt.FlatMap(func(value int) mo.Option[int] {
    if value &gt; 50 {
        return mo.Some(value * 2)
    }
    return mo.None[int]()
}).OrElse(0)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">val result = Some(42)
    .Map((x) =&gt; x * 2)
    .FlatMap((x int) =&gt; if (x &gt; 50) Some(x) else None[int]())
    .GetOrElse(0)
</code></pre>
<p>The API shapes are similar -- both support Map, FlatMap, and a default value fallback. But GALA's version benefits from shorter lambda syntax and the ability to use <code>if/else</code> as an expression (it returns a value, so no curly braces or explicit return needed).</p>
<p>The real difference shows up when you pattern match on the result:</p>
<p><strong>samber/mo:</strong></p>
<pre><code class="language-go">either := mo.Right[string, int](42)
either.Match(
    func(err string) { fmt.Println("error:", err) },
    func(val int) { fmt.Println("value:", val) },
)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">val r1 = validateAge(25) match {
    case Right(age) =&gt; s"Valid age: $age"
    case Left(err)  =&gt; s"Error: $err"
}
</code></pre>
<p>mo's <code>Match</code> takes two function callbacks. GALA's <code>match</code> is a language construct that destructures the value, binds variables, and returns a result -- all in one expression. The GALA version is also exhaustive: if <code>Either</code> had a third variant, the compiler would reject incomplete matches.</p>
<h2>The three things Go lacks that no library can fix</h2>
<p>lo and mo are pushing against the boundaries of what Go's type system allows. Three specific limitations shape every API decision:</p>
<p><strong>1. Lambda syntax.</strong> Go's anonymous functions require <code>func</code>, parameter types, return type, and braces. This is fine for callbacks that span multiple lines, but painful for one-liners. <code>func(x int, _ int) bool { return x%2 == 0 }</code> is a lot of syntax for "is this number even?" GALA's <code>(x) =&gt; x % 2 == 0</code> says the same thing with less noise, because parameter types are inferred from context.</p>
<p><strong>2. Type inference for generics.</strong> Go can infer type parameters in some cases, but not all. mo requires <code>mo.None[int]()</code> -- you cannot write <code>mo.None()</code> and have the compiler figure out <code>T=int</code> from context. GALA's type inference resolves generic parameters from usage: <code>Some(42)</code> is <code>Option[int]</code> without annotation. <code>FoldLeft(0, (acc, x) =&gt; acc + x)</code> infers that the accumulator is <code>int</code> from the zero value.</p>
<p><strong>3. Algebraic data types.</strong> This is the big one. Go has no sum types. mo implements Option as a struct with a boolean flag and a value field. Either is a struct with a flag indicating left or right. These work, but the compiler cannot enforce exhaustive handling. You can forget to check <code>IsPresent()</code> before calling <code>MustGet()</code>, and the compiler will not stop you. GALA's sealed types are true algebraic data types with compiler-enforced exhaustiveness:</p>
<pre><code class="language-gala">sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
    case Point()
}

func describe(s Shape) string = s match {
    case Circle(r)       =&gt; f"circle with radius=$r%.2f"
    case Rectangle(w, h) =&gt; s"rectangle \({w}x\){h}"
    case Point()         =&gt; "a point"
}
</code></pre>
<p>Add a new variant to <code>Shape</code> and every match expression that does not handle it becomes a compile error. No library in Go can provide this guarantee, because Go's type system does not support closed type hierarchies.</p>
<h2>What GALA adds beyond lo and mo</h2>
<p>Beyond cleaner syntax for the same operations, GALA provides constructs that have no library equivalent:</p>
<p><strong>Immutability by default.</strong> Variables declared with <code>val</code> cannot be reassigned. Struct fields are immutable unless explicitly marked <code>var</code>. Every struct gets an auto-generated <code>Copy</code> method for creating modified versions. This is not a convention -- it is compiler-enforced.</p>
<p><strong>String interpolation.</strong> <code>s"Hello \(name"</code> and <code>f"\)value%.2f"</code> replace <code>fmt.Sprintf</code> calls. Format verbs are inferred from the variable type. No import needed.</p>
<p><strong>Expression-oriented syntax.</strong> <code>if/else</code>, <code>match</code>, and single-expression functions all return values. <code>func square(x int) int = x * x</code> -- no braces, no return keyword. This removes an entire category of "forgot to return" bugs.</p>
<p><strong>Full Go interop.</strong> GALA transpiles to Go. Every Go package is importable. Every Go type is usable. There is no FFI boundary, no wrapper generation, no runtime overhead beyond what the transpiler generates. If you use <code>net/http</code>, <code>database/sql</code>, or any Go library, it works directly.</p>
<h2>When to use lo/mo vs GALA</h2>
<p><strong>Use lo/mo when:</strong></p>
<ul>
<li><p>You have an existing Go codebase and want incremental improvements</p>
</li>
<li><p>Your team is comfortable with Go and does not want to learn a new syntax</p>
</li>
<li><p>You need a few specific utilities (lo's <code>Chunk</code>, <code>GroupBy</code>, <code>Uniq</code>) without adopting a new language</p>
</li>
<li><p>You want to stay within the official Go toolchain</p>
</li>
</ul>
<p><strong>Consider GALA when:</strong></p>
<ul>
<li><p>You are starting a new Go-targeted project and want stronger type guarantees from day one</p>
</li>
<li><p>Your domain has complex state machines, protocol types, or AST-like structures that benefit from sealed types</p>
</li>
<li><p>You want immutability enforced by the compiler, not by convention</p>
</li>
<li><p>You come from Scala, Kotlin, or Rust and want similar expressiveness with Go's deployment model</p>
</li>
<li><p>You are building data transformation pipelines where functional collection operations dominate the code</p>
</li>
</ul>
<h2>Honest tradeoffs</h2>
<p>GALA is not a free upgrade. Here is what you give up:</p>
<p><strong>Ecosystem maturity.</strong> Go has world-class tooling: debuggers, profilers, IDE support, error messages refined over 15 years. GALA has an IntelliJ plugin with syntax highlighting and completion, but it is not at the same level. You will hit rough edges.</p>
<p><strong>Team familiarity.</strong> lo is Go code. Any Go developer can read it, debug it, and contribute to it. GALA requires learning a new syntax. The concepts (sealed types, pattern matching, FoldLeft) are well-established in other languages, but they require investment from developers who have not used them before.</p>
<p><strong>Compilation step.</strong> GALA adds a transpilation step before Go compilation. With Bazel, this is seamless. Without it, you are running <code>gala build</code> before <code>go build</code>.</p>
<p><strong>Community size.</strong> lo has 21,000 stars and a large contributor base. GALA is a younger project. The standard library is solid -- Option, Either, Try, Future, six collection types -- but the ecosystem around it is smaller.</p>
<p>These are real costs. Whether the expressiveness gains justify them depends on your team, your domain, and your tolerance for early-adopter friction.</p>
<h2>Try it</h2>
<p>GALA has a browser-based playground at <a href="https://gala-playground.fly.dev">gala-playground.fly.dev</a> with built-in examples. No installation required.</p>
<p>For local development: download a binary from <a href="https://github.com/martianoff/gala/releases">GitHub releases</a>, or build from source with Bazel. GALA is at v0.17.1, with binaries for Linux, macOS, and Windows.</p>
<p>The 21,000 stars on samber/lo are not just a success story for a library. They are evidence that a significant segment of Go developers wants functional programming patterns -- and is willing to adopt third-party dependencies to get them. GALA asks: what if those patterns were not bolted on, but built in?</p>
<hr />
<p><em>I am the author of GALA. Source:</em> <a href="https://github.com/martianoff/gala"><em>github.com/martianoff/gala</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Library vs Language: Two Approaches to Functional Programming in Go]]></title><description><![CDATA[If you have used IBM's fp-go library, you know the promise of functional programming in Go -- and the pain of Go's syntax fighting you every step of the way. Pipe3, Pipe5, Pipe7. Function adapters eve]]></description><link>https://martianov.dev/library-vs-language-two-approaches-to-functional-programming-in-go</link><guid isPermaLink="true">https://martianov.dev/library-vs-language-two-approaches-to-functional-programming-in-go</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 15 Mar 2026 21:48:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64d53a91055f78c80c13d0f6/8e6596c1-784a-4330-b222-61517161f409.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>If you have used IBM's fp-go library, you know the promise of functional programming in Go -- and the pain of Go's syntax fighting you every step of the way. Pipe3, Pipe5, Pipe7. Function adapters everywhere. Type parameters that stretch across the screen. You know the patterns work because you have seen them in Haskell or fp-ts, but the Go rendition feels like wearing a suit two sizes too small.</p>
<p>fp-go is a serious library. With nearly 2,000 GitHub stars and a design rooted in fp-ts, it brings real functional abstractions to Go: Option, Either, IO, Reader, State, and more. The engineering is impressive. But it also exposes a fundamental tension: Go's syntax was not designed for this style of programming, and no library can fully bridge that gap.</p>
<p>This article compares two approaches to the same problem: fp-go (the library approach) and GALA (the language approach). Both target developers who want FP patterns in Go-compatible code. They make very different trade-offs.</p>
<p><em>Disclosure: I am the author of GALA. I have tried to present both approaches fairly, but you should know where I stand. I have genuine respect for fp-go's engineering -- it pushed me to think about what a language-level solution should look like.</em></p>
<h2>The Library Approach: fp-go</h2>
<p>fp-go models functional abstractions as Go packages. Option lives in <code>github.com/IBM/fp-go/option</code>. Either lives in <code>github.com/IBM/fp-go/either</code>. Composition happens through Pipe functions that chain operations sequentially.</p>
<p>Here is a typical fp-go Option pipeline. You have a value that might be absent, and you want to transform it:</p>
<pre><code class="language-go">import (
    O "github.com/IBM/fp-go/option"
    "github.com/IBM/fp-go/function"
)

result := function.Pipe3(
    O.Some(42),
    O.Map(func(x int) int { return x * 2 }),
    O.Chain(func(x int) O.Option[int] {
        if x &gt; 50 {
            return O.Some(x)
        }
        return O.None[int]()
    }),
    O.GetOrElse(func() int { return 0 }),
)
</code></pre>
<p>This works. The types are correct, the behavior is well-defined. But look at what Go's syntax demands: explicit <code>func</code> keywords on every lambda, full type annotations on every parameter and return, separate imports for each module, and a Pipe3 function (not Pipe2, not Pipe4 -- the arity must match the number of operations).</p>
<p>Error handling with Either follows the same pattern:</p>
<pre><code class="language-go">import (
    E "github.com/IBM/fp-go/either"
    "github.com/IBM/fp-go/function"
)

result := function.Pipe2(
    E.Right[string](25),
    E.Chain(func(age int) E.Either[string, int] {
        if age &lt; 0 {
            return E.Left[int]("age cannot be negative")
        }
        return E.Right[string](age)
    }),
    E.Fold(
        func(err string) string { return "Error: " + err },
        func(age int) string { return fmt.Sprintf("Valid age: %d", age) },
    ),
)
</code></pre>
<p>The signal-to-noise ratio is the issue. The business logic -- "validate that age is non-negative" -- occupies a fraction of the code. The rest is structural: type annotations, function wrappers, pipe plumbing.</p>
<p>This is not a criticism of fp-go's design. It is an observation about Go as a host language for FP patterns. Go lacks three things that FP-heavy code relies on: concise lambda syntax, type inference for function parameters, and method chaining on generic types. No library can add these to Go.</p>
<h2>The Language Approach: GALA</h2>
<p>GALA is a language that transpiles to Go. It generates standard Go code, produces native binaries, and interoperates with every Go library. But it has its own syntax designed around functional patterns.</p>
<p>Here is the same Option pipeline in GALA:</p>
<pre><code class="language-gala">package main

func main() {
    val result = Some(42)
        .Map((x) =&gt; x * 2)
        .FlatMap((x int) =&gt; if (x &gt; 50) Some(x) else None[int]())
        .GetOrElse(0)
    Println(result)
}
</code></pre>
<p>No imports for Option -- it is a built-in type. No <code>func</code> keyword on lambdas. No Pipe function -- method chaining works directly. The lambda parameter type in <code>Map</code> is inferred from the Option's type parameter. The code reads as a description of the transformation, not a fight with the type system.</p>
<p>The difference is not cosmetic. It changes how much code you can absorb at a glance, which matters when you are reading a codebase you did not write.</p>
<h2>Side-by-Side: Option Handling</h2>
<p><strong>fp-go:</strong></p>
<pre><code class="language-go">result := function.Pipe3(
    O.Some(42),
    O.Map(func(x int) int { return x * 2 }),
    O.Chain(func(x int) O.Option[int] {
        if x &gt; 50 { return O.Some(x) }
        return O.None[int]()
    }),
    O.GetOrElse(func() int { return 0 }),
)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">val result = Some(42)
    .Map((x) =&gt; x * 2)
    .FlatMap((x int) =&gt; if (x &gt; 50) Some(x) else None[int]())
    .GetOrElse(0)
</code></pre>
<p>Both produce the same result: 84 (since 42 * 2 = 84, which is &gt; 50). The GALA version is roughly a third of the line count. More importantly, the transformation pipeline is visible without mentally filtering out syntax.</p>
<h2>Side-by-Side: Error Handling with Try</h2>
<p>fp-go handles fallible operations through Either and IO. GALA provides a Try monad that wraps operations that might throw, similar to Scala's Try:</p>
<p><strong>fp-go (Either-based):</strong></p>
<pre><code class="language-go">import (
    E "github.com/IBM/fp-go/either"
    "github.com/IBM/fp-go/function"
    "strconv"
)

result := function.Pipe2(
    E.TryCatchError(func() (int, error) {
        return strconv.Atoi("42")
    }),
    E.Map[error](func(n int) int { return n * 2 }),
    E.GetOrElse(func(err error) int { return -1 }),
)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">package main

import "strconv"

func safeParse(s string) Try[int] = Try(() =&gt; strconv.Atoi(s))

func main() {
    val result = safeParse("42")
        .Map((n) =&gt; n * 2)
        .Recover((e) =&gt; -1)
    Println(result)

    val failed = safeParse("not_a_number")
        .Map((n) =&gt; n * 2)
        .Recover((e) =&gt; -1)
    Println(failed)
}
</code></pre>
<p>GALA's <code>Try</code> automatically handles Go's <code>(T, error)</code> multi-return convention. The lambda <code>() =&gt; strconv.Atoi(s)</code> returns <code>(int, error)</code>, and <code>Try</code> converts a non-nil error into a <code>Failure</code>. No explicit error type parameters, no TryCatchError adapter.</p>
<h2>Side-by-Side: Collections</h2>
<p>Functional collection processing is where the verbosity gap becomes stark.</p>
<p><strong>fp-go:</strong></p>
<pre><code class="language-go">import (
    A "github.com/IBM/fp-go/array"
    "github.com/IBM/fp-go/function"
)

nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
result := function.Pipe3(
    nums,
    A.Filter(func(x int) bool { return x%2 == 0 }),
    A.Map(func(x int) int { return x * x }),
    A.Reduce(func(acc int, x int) int { return acc + x }, 0),
)
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">package main

import . "martianoff/gala/collection_immutable"

func main() {
    val nums = ArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val result = nums
        .Filter((x) =&gt; x % 2 == 0)
        .Map((x) =&gt; x * x)
        .FoldLeft(0, (acc, x) =&gt; acc + x)
    Println(s"Sum of squares of evens: $result")
}
</code></pre>
<p>Both compute the same value: the sum of squares of even numbers from 1 to 10 (220). The GALA version chains methods on the collection itself. The fp-go version pipes the collection through free functions. Both are valid compositional styles, but method chaining eliminates the Pipe scaffolding and makes the data flow more immediately visible.</p>
<h2>The Hidden Cost: Type Annotations</h2>
<p>The examples above hint at it, but it is worth calling out explicitly: fp-go requires you to annotate types that GALA infers automatically.</p>
<p>In fp-go, every <code>Map</code>, <code>Chain</code>, and <code>Fold</code> call needs explicit type parameters because Go's generic inference does not propagate through free functions:</p>
<pre><code class="language-go">// fp-go: you must spell out every type
O.Map[int](func(x int) int { return x * 2 })
A.Filter(func(x int) bool { return x%2 == 0 })
A.Map(func(x int) int { return x * x })
A.Reduce(func(acc int, x int) int { return acc + x }, 0)
E.Chain(func(age int) E.Either[string, int] { ... })
</code></pre>
<p>In GALA, the transpiler infers lambda parameter types from the method's generic context. When you call <code>.Map</code> on an <code>Array[int]</code>, the transpiler knows the lambda parameter is <code>int</code>. When you call <code>.FoldLeft(0, ...)</code>, it infers the accumulator type from the zero value:</p>
<pre><code class="language-gala">// GALA: types inferred from context
nums.Filter((x) =&gt; x % 2 == 0)          // x is int (from Array[int])
nums.Map((x) =&gt; x * x)                  // x is int, return is int
nums.FoldLeft(0, (acc, x) =&gt; acc + x)   // acc is int (from 0), x is int
</code></pre>
<p>This is not just fewer characters. It changes how you read code. In the fp-go version, your eyes parse type annotations before reaching the logic. In the GALA version, the logic is all there is. The types are still checked at compile time -- they are just inferred rather than written.</p>
<p>The inference extends to method type parameters too. <code>list.Map((x) =&gt; x.Name)</code> on a <code>List[Person]</code> infers that <code>x</code> is <code>Person</code> and the result is <code>List[string]</code>, without writing <code>Map[string]</code> or annotating <code>x</code>.</p>
<h2>Side-by-Side: Pattern Matching</h2>
<p>This is where fp-go hits a hard wall. Go has no pattern matching syntax, so fp-go uses Fold functions as eliminators:</p>
<p><strong>fp-go:</strong></p>
<pre><code class="language-go">E.Fold(
    func(err string) string { return "Error: " + err },
    func(age int) string { return fmt.Sprintf("Valid age: %d", age) },
)(validateAge(25))
</code></pre>
<p><strong>GALA:</strong></p>
<pre><code class="language-gala">package main

func validateAge(age int) Either[string, int] {
    if (age &lt; 0) {
        return Left("age cannot be negative")
    } else if (age &gt; 150) {
        return Left("age seems unrealistic")
    } else {
        return Right(age)
    }
}

func main() {
    val r1 = validateAge(25) match {
        case Right(age) =&gt; s"Valid age: $age"
        case Left(err)  =&gt; s"Error: $err"
    }
    Println(r1)

    val r2 = validateAge(-5) match {
        case Right(age) =&gt; s"Valid age: $age"
        case Left(err)  =&gt; s"Error: $err"
    }
    Println(r2)
}
</code></pre>
<p>Fold is functionally equivalent to pattern matching. It is also harder to read at scale. When you have nested matches, guards, or multiple extracted values, the anonymous-function-per-branch style becomes difficult to follow. GALA's <code>match</code> expression reads like a conditional: each branch is a pattern, an optional guard, and a result.</p>
<p>GALA also provides sealed types -- something fp-go cannot offer at all:</p>
<pre><code class="language-gala">package main

sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
    case Point()
}

func describe(s Shape) string = s match {
    case Circle(r)       =&gt; f"circle with radius=$r%.2f"
    case Rectangle(w, h) =&gt; s"rectangle \({w}x\){h}"
    case Point()         =&gt; "a point"
}

func main() {
    Println(describe(Circle(3.14)))
    Println(describe(Rectangle(10, 5)))
    Println(describe(Point()))
}
</code></pre>
<p>If you add a new variant to Shape and forget to handle it in a match expression, the compiler rejects your code. This is compile-time exhaustiveness checking -- something that no Go library can provide because Go's type system does not support closed type hierarchies.</p>
<h2>When to Use Which</h2>
<p><strong>Choose fp-go when:</strong></p>
<ul>
<li><p>You need to stay in pure Go. Your team, your CI, your tooling -- all Go. fp-go is a dependency, not a language change.</p>
</li>
<li><p>You want specific abstractions (IO, Reader, State) without adopting a new syntax. fp-go's module coverage is broad.</p>
</li>
<li><p>You are adding FP patterns to an existing Go codebase incrementally. Import one package, use it in one function, expand from there.</p>
</li>
<li><p>You want a mature community around Haskell-style typeclasses in Go. fp-go's design is principled and well-documented.</p>
</li>
</ul>
<p><strong>Choose GALA when:</strong></p>
<ul>
<li><p>You want FP to feel native, not bolted on. Lambdas, type inference, and method chaining are part of the syntax.</p>
</li>
<li><p>You need sealed types and exhaustive pattern matching. No Go library can provide compile-time exhaustiveness.</p>
</li>
<li><p>You are starting a new project and want Go's operational characteristics (native binaries, goroutines, the Go ecosystem) with more expressive syntax.</p>
</li>
<li><p>You come from Scala, Kotlin, or Rust and want similar idioms without the JVM or borrow checker.</p>
</li>
</ul>
<h2>Honest Trade-offs</h2>
<p>GALA adds a transpilation step. Your source files are <code>.gala</code>, not <code>.go</code>. This means:</p>
<ul>
<li><p><strong>Build tooling.</strong> GALA uses Bazel for its build system and provides <code>gala run</code> for quick iteration. This is an additional tool in your stack.</p>
</li>
<li><p><strong>Debugging.</strong> You debug the generated Go code, not the GALA source. The generated code is readable, but it is an extra layer of indirection.</p>
</li>
<li><p><strong>Community size.</strong> fp-go has nearly 2,000 GitHub stars and an active community. GALA is newer and smaller. You will find fewer Stack Overflow answers and fewer blog posts.</p>
</li>
<li><p><strong>IDE support.</strong> GALA has an IntelliJ plugin with syntax highlighting and completion. It is not at the level of Go's LSP-powered tooling.</p>
</li>
<li><p><strong>Maturity.</strong> fp-go has been battle-tested in production at IBM. GALA is at v0.17.1 -- functional and improving, but younger.</p>
</li>
</ul>
<p>On the other side:</p>
<ul>
<li><p><strong>fp-go's verbosity is structural, not fixable.</strong> Go will likely never get concise lambda syntax or HKT. The Pipe/Map/Chain ceremony is permanent.</p>
</li>
<li><p><strong>fp-go cannot add new syntax.</strong> Sealed types, pattern matching, <code>val</code> immutability, string interpolation -- these require language-level support.</p>
</li>
<li><p><strong>fp-go's learning curve is steep in its own way.</strong> Understanding <code>Pipe5(value, F.Map(...), F.Chain(...), F.Fold(...), ...)</code> requires internalizing a convention that has no visual affordance in Go.</p>
</li>
</ul>
<h2>Try It</h2>
<p>GALA has an online playground where you can write and run code in the browser, no installation required:</p>
<p><a href="https://gala-playground.fly.dev"><strong>gala-playground.fly.dev</strong></a></p>
<p>The playground includes examples for Option chaining, pattern matching, sealed types, collections, and Go interop. If the code samples in this article looked interesting, the playground is the fastest way to get a feel for the language.</p>
<p>Source code and documentation: <a href="https://github.com/martianoff/gala">github.com/martianoff/gala</a></p>
<hr />
<p>fp-go and GALA represent two legitimate responses to the same observation: Go's syntax makes functional patterns unnecessarily verbose. fp-go works within Go's constraints, accepting the ceremony as the cost of staying in the ecosystem. GALA steps outside those constraints, accepting a transpilation step as the cost of cleaner syntax. Neither is wrong. The right choice depends on how much of your codebase is FP-heavy, how important compile-time exhaustiveness is to your domain, and whether your team is willing to adopt a new language versus a new library.</p>
<p>If you are already using fp-go and find yourself wishing Go's lambdas were shorter, its type inference were better, and its type system supported sum types -- that is exactly the gap GALA was built to fill.</p>
]]></content:encoded></item><item><title><![CDATA[Pattern Matching in Go: How GALA Brings Sealed Types and Exhaustive Matching to the Go Ecosystem]]></title><description><![CDATA[Go is a language built on simplicity. But simplicity has costs, and one cost Go developers feel acutely is the lack of pattern matching and algebraic data types. If you have ever modeled a closed set ]]></description><link>https://martianov.dev/pattern-matching-in-go-how-gala-brings-sealed-types-and-exhaustive-matching-to-the-go-ecosystem</link><guid isPermaLink="true">https://martianov.dev/pattern-matching-in-go-how-gala-brings-sealed-types-and-exhaustive-matching-to-the-go-ecosystem</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[programming languages]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 15 Mar 2026 18:12:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64d53a91055f78c80c13d0f6/aa5d90af-297e-4d27-8f7c-6cb78dc8e346.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Go is a language built on simplicity. But simplicity has costs, and one cost Go developers feel acutely is the lack of pattern matching and algebraic data types. If you have ever modeled a closed set of variants -- shapes, AST nodes, API responses, state machine states -- you know the pain: interfaces, type switches, zero exhaustiveness checking, and boilerplate that grows with every new variant.</p>
<p>GALA is a programming language that transpiles to Go, bringing sealed types, exhaustive pattern matching, and algebraic data types to the Go ecosystem with zero runtime overhead. The output is plain Go code. Every Go library works. Every Go tool works. You just get better type safety where it matters.</p>
<p>This post walks through the problem, the solution, and real code you can try today.</p>
<hr />
<h2>The Problem: Modeling Variants in Go</h2>
<p>Suppose you are building a graphics library and need to represent shapes. In Go, the standard approach uses interfaces and type switches:</p>
<pre><code class="language-go">type Shape interface {
    isShape()
}

type Circle struct {
    Radius float64
}
func (c Circle) isShape() {}

type Rectangle struct {
    Width  float64
    Height float64
}
func (r Rectangle) isShape() {}

type Triangle struct {
    Base   float64
    Height float64
}
func (t Triangle) isShape() {}
</code></pre>
<p>To compute the area, you write a type switch:</p>
<pre><code class="language-go">func area(s Shape) string {
    switch v := s.(type) {
    case Circle:
        return fmt.Sprintf("circle area: %.2f", math.Pi*v.Radius*v.Radius)
    case Rectangle:
        return fmt.Sprintf("rect area: %.2f", v.Width*v.Height)
    case Triangle:
        return fmt.Sprintf("triangle area: %.2f", 0.5*v.Base*v.Height)
    default:
        return "unknown shape"
    }
}
</code></pre>
<p>This works. But it has three problems that get worse as your codebase grows:</p>
<ol>
<li><p><strong>No exhaustiveness checking.</strong> Add a new <code>Pentagon</code> variant. The compiler says nothing. The <code>default</code> branch silently absorbs it, or worse, you forget the <code>default</code> and get a zero-value return. You find the bug in production.</p>
</li>
<li><p><strong>Boilerplate.</strong> Every variant needs a struct, a marker method, and the type switch needs a <code>default</code> case. The interface itself (<code>isShape()</code>) carries no information -- it exists only to close the type set, and it does not even do that reliably since any package can implement it.</p>
</li>
<li><p><strong>No destructuring.</strong> You cannot extract fields inline. You match the type, then access fields on the bound variable. This is verbose and error-prone when variants have many fields.</p>
</li>
</ol>
<p>These issues have been discussed in Go proposals for years (<a href="https://github.com/golang/go/issues/19412">golang/go#19412</a>, <a href="https://github.com/golang/go/issues/41716">golang/go#41716</a>). Sum types and pattern matching remain among the most requested features. The Go team has chosen not to add them, and that is a reasonable decision for the language's goals. But the need does not go away.</p>
<hr />
<h2>Enter GALA</h2>
<p><a href="https://github.com/martianoff/gala">GALA</a> (Go Alternative Language) transpiles to Go. You write GALA source, the transpiler produces standard Go code, and you build the result with <code>go build</code> or Bazel. There is no runtime library, no reflection magic, no code generation at runtime. The generated Go is the kind of code you would write by hand -- just more of it, and correct.</p>
<p>GALA gives you sealed types (algebraic data types), exhaustive pattern matching with destructuring and guards, <code>Option[T]</code>/<code>Either[A,B]</code>/<code>Try[T]</code> monads, immutable collections, and full Go interop. You can import any Go package, call any Go function, and use any Go library.</p>
<p>The trade-off is honest: you add a transpilation step. Your source files are <code>.gala</code> instead of <code>.go</code>. IDE support is available for IntelliJ but not yet universal. Whether that trade-off is worth it depends on your project. For codebases heavy on variant types, state machines, or functional data processing, it often is.</p>
<hr />
<h2>Sealed Types: Closed Type Hierarchies Done Right</h2>
<p>Here is the same Shape example in GALA:</p>
<pre><code class="language-gala">sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
    case Triangle(Base float64, Height float64)
}
</code></pre>
<p>That is the entire definition. The transpiler generates:</p>
<ul>
<li><p>A parent struct <code>Shape</code> with a <code>_variant</code> discriminator field</p>
</li>
<li><p>Companion objects <code>Circle{}</code>, <code>Rectangle{}</code>, <code>Triangle{}</code> with <code>Apply</code> methods for construction</p>
</li>
<li><p><code>Unapply</code> methods for pattern matching (destructuring)</p>
</li>
<li><p><code>IsCircle()</code>, <code>IsRectangle()</code>, <code>IsTriangle()</code> discriminator methods</p>
</li>
<li><p><code>Copy</code> and <code>Equal</code> methods</p>
</li>
</ul>
<p>Construction is concise:</p>
<pre><code class="language-gala">val c = Circle(3.14)
val r = Rectangle(10.0, 20.0)
</code></pre>
<p>Sealed types also support generics:</p>
<pre><code class="language-gala">sealed type Result[T any] {
    case Ok(Value T)
    case Err(Error error)
}

val success = Ok(42)
val failure = Err[int](fmt.Errorf("not found"))
</code></pre>
<p>Compare this to the Go version. The GALA definition is 4 lines. The Go version is 18 lines of structs and marker methods, and it still does not give you exhaustiveness checking or destructuring.</p>
<hr />
<h2>Pattern Matching: Beyond the Type Switch</h2>
<p>GALA's <code>match</code> expression supports literal matching, variable binding, destructuring, guards, nested patterns, and type-based matching. Here is the area function:</p>
<pre><code class="language-gala">func area(s Shape) string = s match {
    case Circle(r)       =&gt; f"circle area: ${3.14159 * r * r}%.2f"
    case Rectangle(w, h) =&gt; f"rect area: ${w * h}%.2f"
    case Triangle(b, h)  =&gt; f"triangle area: ${0.5 * b * h}%.2f"
}
</code></pre>
<p>Notice what is different from Go:</p>
<ul>
<li><p><strong>Destructuring.</strong> <code>case Circle(r)</code> binds the <code>Radius</code> field to <code>r</code> in one step. No <code>v := s.(Circle); r := v.Radius</code>.</p>
</li>
<li><p><strong>Expression syntax.</strong> The entire match is an expression that returns a value. No <code>var result string</code> declaration before the switch.</p>
</li>
<li><p><strong>No default case.</strong> All three variants are covered. The compiler knows this. If you remove the <code>Triangle</code> branch, you get a compile error.</p>
</li>
</ul>
<h3>Guards</h3>
<p>Guards add conditions to patterns:</p>
<pre><code class="language-gala">struct Person(Name string, Age int)

val status = person match {
    case Person(name, age) if age &lt; 18 =&gt; s"$name is a minor"
    case Person(name, age) if age &gt; 65 =&gt; s"$name is a senior"
    case Person(name, _)               =&gt; s"$name is an adult"
}
</code></pre>
<p>The equivalent Go code needs a type switch (or if-else chain), manual field access, and a mutable variable to hold the result:</p>
<pre><code class="language-go">var status string
switch {
case person.Age &lt; 18:
    status = fmt.Sprintf("%s is a minor", person.Name)
case person.Age &gt; 65:
    status = fmt.Sprintf("%s is a senior", person.Name)
default:
    status = fmt.Sprintf("%s is an adult", person.Name)
}
</code></pre>
<h3>Nested Patterns</h3>
<p>Patterns compose. You can match extractors inside extractors:</p>
<pre><code class="language-gala">type Even struct {}
func (e Even) Unapply(i int) Option[int] = if (i % 2 == 0) Some(i) else None[int]()

val opt = Some(10)
opt match {
    case Some(Even(n)) =&gt; Println(s"even number: $n")
    case Some(n)       =&gt; Println(s"odd number: $n")
    case None()        =&gt; Println("nothing")
}
</code></pre>
<p>Try writing that with Go type switches. You would need nested switches, temporary variables, and manual extractor logic. In GALA, the intent is clear in a single expression.</p>
<h3>Type-Based Matching</h3>
<p>When working with <code>any</code> or interface types, GALA supports type patterns directly:</p>
<pre><code class="language-gala">val x any = "hello"

val res = x match {
    case s: string =&gt; "string: " + s
    case i: int    =&gt; s"int: $i"
    case _         =&gt; "unknown"
}
</code></pre>
<p>This is similar to Go's type switch, but integrated into the same match syntax as everything else.</p>
<hr />
<h2>Exhaustive Matching: The Compiler Catches Missing Cases</h2>
<p>This is where golang sealed types and exhaustive matching really pay off. When you match on a sealed type and miss a variant, the GALA transpiler rejects the code at compile time.</p>
<p>Given this sealed type:</p>
<pre><code class="language-gala">sealed type TrafficLight {
    case Red()
    case Yellow()
    case Green()
}
</code></pre>
<p>This code compiles:</p>
<pre><code class="language-gala">val action = light match {
    case Red()    =&gt; "stop"
    case Yellow() =&gt; "caution"
    case Green()  =&gt; "go"
}
</code></pre>
<p>But remove the <code>Yellow</code> case and the transpiler reports an error: the match is not exhaustive. You cannot forget to handle a variant.</p>
<p>In Go, you would use <code>iota</code> constants or separate types with a type switch, and nothing stops you from forgetting a case. Linters like <code>exhaustive</code> exist but they are optional, require configuration, and only work with <code>iota</code>-based enums. GALA's exhaustiveness checking is built into the language and works with full algebraic data types, not just integer constants.</p>
<p>If you only care about some variants, use a wildcard catch-all:</p>
<pre><code class="language-gala">val msg = light match {
    case Red() =&gt; "must stop"
    case _     =&gt; "can proceed"
}
</code></pre>
<p>This is explicit -- you are telling the compiler you have intentionally handled the remaining cases together.</p>
<hr />
<h2>Real-World Example: Expression Evaluator</h2>
<p>Let us build a simple arithmetic expression evaluator using sealed types and pattern matching. This is a classic use case for go algebraic data types.</p>
<pre><code class="language-gala">package main

sealed type Expr {
    case Num(Value float64)
    case Add(Left Expr, Right Expr)
    case Mul(Left Expr, Right Expr)
}

func eval(e Expr) float64 = e match {
    case Num(v)    =&gt; v
    case Add(l, r) =&gt; eval(l) + eval(r)
    case Mul(l, r) =&gt; eval(l) * eval(r)
}

func show(e Expr) string = e match {
    case Num(v)    =&gt; f"$v%.0f"
    case Add(l, r) =&gt; s"(\({show(l)} + \){show(r)})"
    case Mul(l, r) =&gt; s"(\({show(l)} * \){show(r)})"
}

func main() {
    // (2 + 3) * 4
    val expr = Mul(Add(Num(2), Num(3)), Num(4))
    Println(s"\({show(expr)} = \){eval(expr)}")
}
</code></pre>
<p>Output: <code>((2 + 3) * 4) = 20</code></p>
<p>Now here is the equivalent Go code. It is what the transpiler generates (simplified for clarity):</p>
<pre><code class="language-go">package main

import "fmt"

const (
    Expr_Num uint8 = iota
    Expr_Add
    Expr_Mul
)

type Expr struct {
    _variant uint8
    Value    float64
    Left     Expr
    Right    Expr
}

// Companion objects + Apply methods omitted for brevity

func eval(e Expr) float64 {
    switch e._variant {
    case Expr_Num:
        return e.Value
    case Expr_Add:
        return eval(e.Left) + eval(e.Right)
    case Expr_Mul:
        return eval(e.Left) * eval(e.Right)
    default:
        panic("non-exhaustive match")
    }
}

func show(e Expr) string {
    switch e._variant {
    case Expr_Num:
        return fmt.Sprintf("%.0f", e.Value)
    case Expr_Add:
        return fmt.Sprintf("(%s + %s)", show(e.Left), show(e.Right))
    case Expr_Mul:
        return fmt.Sprintf("(%s * %s)", show(e.Left), show(e.Right))
    default:
        panic("non-exhaustive match")
    }
}

func main() {
    expr := Expr{_variant: Expr_Mul,
        Left: Expr{_variant: Expr_Add,
            Left:  Expr{_variant: Expr_Num, Value: 2},
            Right: Expr{_variant: Expr_Num, Value: 3},
        },
        Right: Expr{_variant: Expr_Num, Value: 4},
    }
    fmt.Printf("%s = %.0f\n", show(expr), eval(expr))
}
</code></pre>
<p>The Go version is roughly three times longer. More importantly, the Go version has no compile-time guarantee that the switch is exhaustive. The <code>default: panic</code> is a runtime safety net, not a compile-time one. Add a <code>Div</code> variant to <code>Expr</code> and the Go compiler says nothing. The GALA compiler rejects the code until you handle <code>Div</code> in every match.</p>
<hr />
<h2>Option, Either, and Try: Standard Library Patterns</h2>
<p>GALA's standard library uses sealed types throughout. These are not special compiler features -- they are regular sealed types that you could define yourself:</p>
<pre><code class="language-gala">sealed type Option[T any] {
    case Some(Value T)
    case None()
}

sealed type Either[A any, B any] {
    case Left(LeftValue A)
    case Right(RightValue B)
}

sealed type Try[T any] {
    case Success(Value T)
    case Failure(Err error)
}
</code></pre>
<p>They all support pattern matching:</p>
<pre><code class="language-gala">val result = fetchUser(id) match {
    case Success(user) =&gt; s"Hello, ${user.Name}"
    case Failure(err)  =&gt; s"Error: $err"
}

val name = user.Email match {
    case Some(email) =&gt; email
    case None()      =&gt; "no email on file"
}
</code></pre>
<p>And they support monadic chaining with <code>Map</code>, <code>FlatMap</code>, <code>Filter</code>, <code>GetOrElse</code>, and <code>Recover</code>:</p>
<pre><code class="language-gala">val displayName = user.Name
    .Map((n) =&gt; strings.ToUpper(n))
    .GetOrElse("ANONYMOUS")

val result = divide(10, 2)
    .Map((x) =&gt; x * 2)
    .FlatMap((x) =&gt; divide(x, 3))
    .Recover((e) =&gt; 0)
</code></pre>
<p>This eliminates entire categories of nil-pointer bugs and forgotten error checks. The types make missing cases visible, and pattern matching makes handling them concise.</p>
<hr />
<h2>Try It Yourself</h2>
<p><strong>In your browser:</strong> The <a href="https://gala-playground.fly.dev">GALA Playground</a> lets you write and run GALA code with no installation.</p>
<p><strong>On your machine:</strong> Download a binary from <a href="https://github.com/martianoff/gala/releases">GitHub Releases</a> for Linux, macOS, or Windows. Then:</p>
<pre><code class="language-bash"># Create a file
cat &gt; main.gala &lt;&lt; 'EOF'
package main

sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
}

func describe(s Shape) string = s match {
    case Circle(r)       =&gt; f"circle with radius $r%.2f"
    case Rectangle(w, h) =&gt; f"\({w}%.0f x \){h}%.0f rectangle"
}

func main() {
    Println(describe(Circle(3.14)))
    Println(describe(Rectangle(10, 5)))
}
EOF

# Run it
gala run main.gala
</code></pre>
<p>For projects, GALA integrates with Bazel:</p>
<pre><code class="language-python">load("//:gala.bzl", "gala_binary")

gala_binary(
    name = "myapp",
    src = "main.gala",
)
</code></pre>
<hr />
<h2>Conclusion</h2>
<p>GALA does not replace Go. It transpiles to Go. The output is standard Go code that uses Go's type system, Go's runtime, and Go's toolchain. There is no runtime dependency, no reflection overhead, and no magic.</p>
<p>What GALA adds is a layer of compile-time safety for a specific class of problems: modeling closed variant types and ensuring every case is handled. If your codebase has state machines, AST nodes, protocol messages, API response types, or any domain with a fixed set of variants, sealed types and exhaustive pattern matching eliminate an entire category of bugs that Go's type system cannot catch.</p>
<p>The trade-off is a transpilation step and a different source language. For many projects, that trade-off is not worth it -- Go's simplicity is its strength. But for projects where type safety at the variant level matters more than syntactic familiarity, GALA gives you go pattern matching that the language itself has not yet provided.</p>
<p>Check out the <a href="https://github.com/martianoff/gala">GitHub repository</a> for the full language specification, or try the <a href="https://gala-playground.fly.dev">playground</a> to experiment with sealed types and pattern matching right now.</p>
]]></content:encoded></item><item><title><![CDATA[Enhancing Golang with Scala-Style Option Handling for Safer Code]]></title><description><![CDATA[As a developer and a fan of Scala and functional languages I really like to have an ability to be able to handle optional or nullable objects safely and be able to chain them to something new. Standard way to handle them in Golang is to handle them i...]]></description><link>https://martianov.dev/enhancing-golang-with-scala-style-option-handling-for-safer-code</link><guid isPermaLink="true">https://martianov.dev/enhancing-golang-with-scala-style-option-handling-for-safer-code</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Optional chaining]]></category><category><![CDATA[Functional Programming]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Thu, 04 Jul 2024 22:26:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2JIvboGLeho/upload/1152cc11cf8b2292fa2104de4f12c8e4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a developer and a fan of Scala and functional languages I really like to have an ability to be able to handle optional or nullable objects safely and be able to chain them to something new. Standard way to handle them in Golang is to handle them iteratively. For example:</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Customer <span class="hljs-keyword">struct</span> {
    firstName <span class="hljs-keyword">string</span>
    car *Car <span class="hljs-comment">// customer might have a car</span>
}

<span class="hljs-keyword">type</span> Car <span class="hljs-keyword">struct</span> {
    plateNumber <span class="hljs-keyword">string</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getCustomerPlateNumber</span><span class="hljs-params">(customer Customer)</span> *<span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">if</span> customer.car != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> &amp;customer.car.plateNumber
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p>To make code more readable, especially if we have to deal with a chain of optional values, I made a library <a target="_blank" href="https://github.com/martianoff/go-option">https://github.com/martianoff/go-option</a> that uses functional language patterns and follows syntax of Scala Option</p>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/martianoff/go-option"</span>

<span class="hljs-keyword">type</span> Customer <span class="hljs-keyword">struct</span> {
    firstName <span class="hljs-keyword">string</span>
    car option.Option[Car] <span class="hljs-comment">// customer might have a car</span>
}

<span class="hljs-keyword">type</span> Car <span class="hljs-keyword">struct</span> {
    plateNumber <span class="hljs-keyword">string</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getCustomerPlateNumber</span><span class="hljs-params">(customer Customer)</span> <span class="hljs-title">option</span>.<span class="hljs-title">Option</span>[<span class="hljs-title">string</span>]</span> {
    <span class="hljs-keyword">return</span> option.Map(customer.car, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(car Car)</span> <span class="hljs-title">string</span></span> {
        <span class="hljs-keyword">return</span> car.plateNumber
    })
}
</code></pre>
<p>Features of the package:</p>
<ul>
<li><p><strong>Safe Handling of Missing Values:</strong> The Option[T] type encourages safer handling of missing values, avoiding null pointer dereferences. It can be checked for its state (whether it's Some or None) before usage, ensuring that no nil value is being accessed.</p>
</li>
<li><p><strong>Functional Methods:</strong> The module provides functional methods such as Map, FlatMap etc. that make operations on Option[T] type instances easy, clear, and less error-prone.</p>
</li>
<li><p><strong>Pattern Matching:</strong> The Match function allows for clean and efficient handling of Option[T] instances. Depending on whether the Option[T] is Some or None, corresponding function passed to Match get executed, which makes the code expressive and maintains safety.</p>
</li>
<li><p><strong>Equality Check:</strong> The Equal method provides an efficient way to compare two Option[T] instances, checking if they represent the same state and hold equal values.</p>
</li>
<li><p><strong>Generics-Based:</strong> With Generics introduced in Go, the module offers a powerful, type-safe alternative to previous ways of handling optional values.</p>
</li>
<li><p><strong>Json serialization and deserialization:</strong> Build-in Json support</p>
</li>
</ul>
<p>By leveraging functional language patterns, developers can handle optional or nullable objects more effectively, reducing the risk of null pointer dereferences.</p>
]]></content:encoded></item><item><title><![CDATA[Unlocking Scala Mastery with my leetcode Solutions Archive]]></title><description><![CDATA[Introduction
I have decided to dump my personal leetcode solutions archive. Currently it contains Scala solutions only. I will eventually publish all the remaining solutions in GO, JS and PHP and others.
These solutions have been collected over years...]]></description><link>https://martianov.dev/unlocking-scala-mastery-with-my-leetcode-solutions-archive</link><guid isPermaLink="true">https://martianov.dev/unlocking-scala-mastery-with-my-leetcode-solutions-archive</guid><category><![CDATA[Scala]]></category><category><![CDATA[leetcode]]></category><category><![CDATA[leetcode-solution]]></category><category><![CDATA[Functional Programming]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sat, 18 May 2024 19:58:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716062103260/e346876b-4a11-4137-ad1f-2c0f3bd50d99.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction">Introduction</h3>
<p>I have decided to dump my personal leetcode solutions archive. Currently it contains <strong>Scala</strong> solutions only. I will eventually publish all the remaining solutions in GO, JS and PHP and others.</p>
<p>These solutions have been collected over years and might have slightly different style, which evolved over time.</p>
<h3 id="heading-purpose-of-the-project">Purpose of the Project</h3>
<p>The purpose of this project is not to endorse the mere copying of solutions but to learn, understand and internalize the logic and elegance of problem-solving in <code>Scala</code>. So, seize this opportunity to deepen your knowledge and become a better developer.</p>
<h3 id="heading-access-the-archive">Access the Archive</h3>
<p>The link to the archive is <a target="_blank" href="https://github.com/martianoff/leetcode-archive">https://github.com/martianoff/leetcode-archive</a>. Enjoy!</p>
]]></content:encoded></item><item><title><![CDATA[Understanding Scala Extractors: An Easy-to-Follow Example]]></title><description><![CDATA[Let's implement a simple case class that represents a Time object that contains hours and minutes. For simplicity, we will use 24-hour clock.
case class Time(hour: Hour, minutes: Minute)
case class Hour(hour: Int)
case class Minute(minute: Int)

Now ...]]></description><link>https://martianov.dev/understanding-scala-extractors-an-easy-to-follow-example</link><guid isPermaLink="true">https://martianov.dev/understanding-scala-extractors-an-easy-to-follow-example</guid><category><![CDATA[example]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Scala]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sat, 12 Aug 2023 03:27:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/UAvYasdkzq8/upload/9c8c7c3a7a2e1a4efee925590b9a1771.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's implement a simple case class that represents a Time object that contains hours and minutes. For simplicity, we will use 24-hour clock.</p>
<pre><code class="lang-scala"><span class="hljs-keyword">case</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Time</span>(<span class="hljs-params">hour: <span class="hljs-type">Hour</span>, minutes: <span class="hljs-type">Minute</span></span>)</span>
<span class="hljs-keyword">case</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Hour</span>(<span class="hljs-params">hour: <span class="hljs-type">Int</span></span>)</span>
<span class="hljs-keyword">case</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Minute</span>(<span class="hljs-params">minute: <span class="hljs-type">Int</span></span>)</span>
</code></pre>
<p>Now let's try to implement a code that parses a string into Time using custom extractor</p>
<pre><code class="lang-scala"><span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Time</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(stringTime: <span class="hljs-type">String</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Time</span>] = {
    stringTime.split(<span class="hljs-string">":"</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">Array</span>(h, m) =&gt;
        <span class="hljs-type">Some</span>(<span class="hljs-type">Time</span>(h, m))
      <span class="hljs-keyword">case</span> _ =&gt; <span class="hljs-type">None</span>
    }
  }
}
</code></pre>
<p>The problem with this code is that hours and minutes are not validated, therefore strings like "30:-1" will still be considered as valid time.</p>
<p>To solve this problem we can embed conditions into time extractor directly or better define separate Hour and Minute extractors with their own validation rules.</p>
<pre><code class="lang-scala"><span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Hour</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(stringHour: <span class="hljs-type">String</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Hour</span>] = {
    stringHour.toIntOption <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">Some</span>(h) <span class="hljs-keyword">if</span> h &gt;= <span class="hljs-number">0</span> &amp;&amp; h &lt;= <span class="hljs-number">23</span> =&gt; <span class="hljs-type">Some</span>(<span class="hljs-type">Hour</span>(h))
      <span class="hljs-keyword">case</span> _ =&gt; <span class="hljs-type">None</span>
    }
  }
}

<span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Minute</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(stringMinute: <span class="hljs-type">String</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Minute</span>] = {
    stringMinute.toIntOption <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">Some</span>(m) <span class="hljs-keyword">if</span> m &gt;= <span class="hljs-number">0</span> &amp;&amp; m &lt;= <span class="hljs-number">59</span> =&gt; <span class="hljs-type">Some</span>(<span class="hljs-type">Minute</span>(m))
      <span class="hljs-keyword">case</span> _ =&gt; <span class="hljs-type">None</span>
    }
  }
}
</code></pre>
<p>These extractors allow to extract minutes and hours from string only if they are valid. Now adjust the Time extractor to use extractors for hours and minutes</p>
<pre><code class="lang-scala"><span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Hour</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(stringHour: <span class="hljs-type">String</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Hour</span>] = {
    stringHour.toIntOption <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">Some</span>(h) <span class="hljs-keyword">if</span> h &gt;= <span class="hljs-number">0</span> &amp;&amp; h &lt;= <span class="hljs-number">23</span> =&gt; <span class="hljs-type">Some</span>(<span class="hljs-type">Hour</span>(h))
      <span class="hljs-keyword">case</span> _ =&gt; <span class="hljs-type">None</span>
    }
  }
}

<span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Minute</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(stringMinute: <span class="hljs-type">String</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Minute</span>] = {
    stringMinute.toIntOption <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">Some</span>(m) <span class="hljs-keyword">if</span> m &gt;= <span class="hljs-number">0</span> &amp;&amp; m &lt;= <span class="hljs-number">59</span> =&gt; <span class="hljs-type">Some</span>(<span class="hljs-type">Minute</span>(m))
      <span class="hljs-keyword">case</span> _ =&gt; <span class="hljs-type">None</span>
    }
  }
}
<span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Time</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(stringTime: <span class="hljs-type">String</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Time</span>] = {
    stringTime.split(<span class="hljs-string">":"</span>,<span class="hljs-number">2</span>) <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">Array</span>(<span class="hljs-type">Hour</span>(h), <span class="hljs-type">Minute</span>(m)) =&gt;
        <span class="hljs-type">Some</span>(<span class="hljs-type">Time</span>(h, m))
      <span class="hljs-keyword">case</span> _ =&gt; <span class="hljs-type">None</span>
    }
  }
}
</code></pre>
<p>Let's test the code on a few unit tests</p>
<pre><code class="lang-scala"><span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">TimeExtractors</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">App</span> </span>{

  <span class="hljs-type">List</span>(<span class="hljs-string">"12:30"</span>, <span class="hljs-string">"-1:23"</span>, <span class="hljs-string">"a:b"</span>, <span class="hljs-string">"4:59"</span>, <span class="hljs-string">"1:2:3"</span>, <span class="hljs-string">"12:77"</span>).foreach {
    <span class="hljs-keyword">case</span> strTime @ <span class="hljs-type">Time</span>(t) =&gt; println(<span class="hljs-string">s"<span class="hljs-subst">${strTime}</span> converted to <span class="hljs-subst">${t}</span>"</span>)
    <span class="hljs-keyword">case</span> strTime =&gt; println(<span class="hljs-string">s"unknown time <span class="hljs-subst">${strTime}</span>"</span>)
  }

}
</code></pre>
<p>Output from the code above</p>
<pre><code class="lang-scala"><span class="hljs-number">12</span>:<span class="hljs-number">30</span> converted to <span class="hljs-type">Time</span>(<span class="hljs-type">Hour</span>(<span class="hljs-number">12</span>),<span class="hljs-type">Minute</span>(<span class="hljs-number">30</span>)) 
unknown time <span class="hljs-number">-1</span>:<span class="hljs-number">23</span> 
unknown time a:b 
<span class="hljs-number">4</span>:<span class="hljs-number">59</span> converted to <span class="hljs-type">Time</span>(<span class="hljs-type">Hour</span>(<span class="hljs-number">4</span>),<span class="hljs-type">Minute</span>(<span class="hljs-number">59</span>)) 
unknown time <span class="hljs-number">1</span>:<span class="hljs-number">2</span>:<span class="hljs-number">3</span> 
unknown time <span class="hljs-number">12</span>:<span class="hljs-number">77</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We can chain any number of extractors as in case Some(Array(Animal(name, Some(breed)))) =&gt; ... to make code more readable</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Symbol <strong>@ </strong>in a case match is called "<strong>Pattern Binder</strong>". It allows to store raw value in a local variable, this is very useful because it is not always possible/cheap to construct the object back</div>
</div>

<blockquote>
<p>This article demonstrates how to implement a simple Time case class with validation for hours and minutes using extractors in Scala and how to combine multiple extractors to increase readability</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Exploring Immutable Arrays in Scala: A Deep Dive into Vectors]]></title><description><![CDATA[Scala Array is not located in the scala.collection.mutable package, however, they are in fact mutable
val arr = Array(1,2,3)
arr(1) = 3
println(arr.mkString(",")) // 1,3,3

Immutable alternative to Scala Array is Scala Vector
val arr = Vector(1,2,3)
...]]></description><link>https://martianov.dev/exploring-immutable-arrays-in-scala-a-deep-dive-into-vectors</link><guid isPermaLink="true">https://martianov.dev/exploring-immutable-arrays-in-scala-a-deep-dive-into-vectors</guid><category><![CDATA[Scala]]></category><category><![CDATA[immutability]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sat, 12 Aug 2023 03:02:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/dUPwMFSIKEE/upload/d199cbc565ee9d94b98809acc27c879e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Scala Array is not located in the <strong>scala.collection.mutable</strong> package, however, they are in fact mutable</p>
<pre><code class="lang-scala"><span class="hljs-keyword">val</span> arr = <span class="hljs-type">Array</span>(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>)
arr(<span class="hljs-number">1</span>) = <span class="hljs-number">3</span>
println(arr.mkString(<span class="hljs-string">","</span>)) <span class="hljs-comment">// 1,3,3</span>
</code></pre>
<p>Immutable alternative to Scala Array is Scala Vector</p>
<pre><code class="lang-scala"><span class="hljs-keyword">val</span> arr = <span class="hljs-type">Vector</span>(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>)
println(arr.updated(<span class="hljs-number">1</span>,<span class="hljs-number">3</span>).mkString(<span class="hljs-string">","</span>)) <span class="hljs-comment">// 1,3,3</span>
println(arr.mkString(<span class="hljs-string">","</span>)) <span class="hljs-comment">// 1,2,3 because original array remains unchanged</span>
</code></pre>
<p>Vector provides O(1) asymptotic time complexity prepend, append, random read, random write operations</p>
<h3 id="heading-most-important-methods-of-vector-class">Most important methods of Vector class:</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>vector.update(index, V)</td><td>random write, returns a new vector with an updated value V at the specified index</td></tr>
<tr>
<td>vector(index)</td><td>random read, returns a value of the index element</td></tr>
<tr>
<td>vector.prepended(V)</td><td>returns a new vector with prepended value V</td></tr>
<tr>
<td>vector.appended(v)</td><td>returns a new vector with appended value V</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Unlock the Power of Scala's Chaining Utils: A Guide to Tap and Pipe]]></title><description><![CDATA[There is an amazing util in Scala that is available in scala.util.chaining._. It adds two implicit methods to any scala object that allow chain commands (pipe) and apply side effects (tap).
Tap example
import scala.util.chaining._

trait StatsCounter...]]></description><link>https://martianov.dev/unlock-the-power-of-scalas-chaining-utils-a-guide-to-tap-and-pipe</link><guid isPermaLink="true">https://martianov.dev/unlock-the-power-of-scalas-chaining-utils-a-guide-to-tap-and-pipe</guid><category><![CDATA[Scala]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Fri, 11 Aug 2023 01:23:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/t1OalCBUYRc/upload/be470c679ea9f0cf80d0edbda06788c4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is an amazing util in Scala that is available in <strong>scala.util.chaining._</strong>. It adds two implicit methods to any scala object that allow chain commands (pipe) and apply side effects (tap).</p>
<h3 id="heading-tap-example">Tap example</h3>
<pre><code class="lang-scala"><span class="hljs-keyword">import</span> scala.util.chaining._

<span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">StatsCounter</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">incr</span></span>(counterName: <span class="hljs-type">String</span>): <span class="hljs-type">Unit</span>
}

<span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Solution</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">App</span> </span>{
      <span class="hljs-keyword">val</span> value = <span class="hljs-number">123</span>
      <span class="hljs-keyword">val</span> counter: <span class="hljs-type">StatsCounter</span> = ... <span class="hljs-comment">// implementation</span>
      stringValue(value)
      <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">stringValue</span></span>(v: <span class="hljs-type">Int</span>): <span class="hljs-type">String</span> = {
           <span class="hljs-comment">// tap adds pure side effect without changing input value</span>
           v.toString.tap { 
                 result =&gt; counter.incr(result)
            }
            <span class="hljs-comment">// this code does the same without chaining</span>
            <span class="hljs-comment">// val result = v.toString</span>
            <span class="hljs-comment">// counter.incr(result)</span>
            <span class="hljs-comment">// result</span>
       }
}
</code></pre>
<h3 id="heading-pipe-example">Pipe example</h3>
<pre><code class="lang-scala"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">calculate</span></span>(value: <span class="hljs-type">Int</span>) = {
      value
     .pipe(_ + <span class="hljs-number">100</span>)
     .pipe(<span class="hljs-type">Some</span>(_)) 
}

calculate(<span class="hljs-number">100</span>) <span class="hljs-comment">// returns Some(200)</span>
</code></pre>
<h3 id="heading-pipe-tap-example">Pipe + Tap example</h3>
<pre><code class="lang-scala"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">calculate</span></span>(value: <span class="hljs-type">Int</span>) = {
     value
     .tap(println)
     .pipe(_ + <span class="hljs-number">100</span>)
     .tap(println)
     .pipe(<span class="hljs-type">Some</span>(_))
     .tap(println)
}

calculate(<span class="hljs-number">100</span>) 
<span class="hljs-comment">/* 
returns Some(200)
prints:
100
200
Some(200)
*/</span>
</code></pre>
<p>Scala's chaining utils provide a powerful and efficient way to streamline code by enabling the use of tap and pipe methods. By leveraging these methods, developers can optimize their functional code, enhance readability, and improve overall productivity.</p>
]]></content:encoded></item><item><title><![CDATA[Safeguarding Collections and Object References with Scala's Option]]></title><description><![CDATA[We often use Option to handle collections safely:
val collection = Map.empty[Int, Int]

// unsafe way that will throw an exception if key is not present
val value = collection(1)

// safe way
val valueOpt = collection.get(1) match {
    case Some(val...]]></description><link>https://martianov.dev/safeguarding-collections-and-object-references-with-scalas-option</link><guid isPermaLink="true">https://martianov.dev/safeguarding-collections-and-object-references-with-scalas-option</guid><category><![CDATA[Scala]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Fri, 11 Aug 2023 01:15:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/XJXWbfSo2f0/upload/2394081eb93c58c7490b2182d7b15c2d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We often use Option to handle collections safely:</p>
<pre><code class="lang-scala"><span class="hljs-keyword">val</span> collection = <span class="hljs-type">Map</span>.empty[<span class="hljs-type">Int</span>, <span class="hljs-type">Int</span>]

<span class="hljs-comment">// unsafe way that will throw an exception if key is not present</span>
<span class="hljs-keyword">val</span> value = collection(<span class="hljs-number">1</span>)

<span class="hljs-comment">// safe way</span>
<span class="hljs-keyword">val</span> valueOpt = collection.get(<span class="hljs-number">1</span>) <span class="hljs-keyword">match</span> {
    <span class="hljs-keyword">case</span> <span class="hljs-type">Some</span>(value) =&gt; <span class="hljs-comment">// key is present</span>
    <span class="hljs-keyword">case</span> <span class="hljs-type">None</span> =&gt; <span class="hljs-comment">// key is not present</span>
}
</code></pre>
<p>In Scala, object references can have a value of null. That makes them unsafe. Bugs, that are related to unsafe calls, are very hard to detect. Luckily, the Option class can help to handle them for free:</p>
<pre><code class="lang-scala"><span class="hljs-keyword">var</span> mutableRef: <span class="hljs-type">Car</span> = <span class="hljs-literal">null</span>
<span class="hljs-type">Option</span>(mutableRef) <span class="hljs-keyword">match</span> {
    <span class="hljs-keyword">case</span> <span class="hljs-type">Some</span>(value) =&gt; <span class="hljs-comment">// non-null</span>
    <span class="hljs-keyword">case</span> <span class="hljs-type">None</span> =&gt; <span class="hljs-comment">// null</span>
}
</code></pre>
<p>This also lets us use Option composition to make safe calls:</p>
<pre><code class="lang-scala"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Car</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">val</span> width: <span class="hljs-type">Int</span> = <span class="hljs-number">100</span>
}

<span class="hljs-keyword">var</span> mutableRef: <span class="hljs-type">Car</span> = <span class="hljs-literal">null</span>

<span class="hljs-comment">// unsafe way that will throw an exception if mutableRef is null</span>
<span class="hljs-keyword">val</span> carWidth = mutableRef.width

<span class="hljs-comment">// safe way</span>
<span class="hljs-keyword">val</span> carWidthOpt = <span class="hljs-type">Option</span>(mutableRef).map(_.width)
</code></pre>
<p>Scala's Option class provides a powerful way to handle null values and safeguard collections and object references, minimizing the risk of bugs related to unsafe calls. By utilizing Option composition, developers can create more robust and maintainable code, ensuring a safer and more efficient development experience.</p>
]]></content:encoded></item><item><title><![CDATA[Learn About Scala's Case Classes and Built-in Extractor Functions]]></title><description><![CDATA[Case classes in Scala are very handy because they have build extractor capability and other free features.
We can define a case class and then use it conveniently in the code:
import scala.collection.mutable

case class MyFeature(a: Int, b: Int, c: I...]]></description><link>https://martianov.dev/learn-about-scalas-case-classes-and-built-in-extractor-functions</link><guid isPermaLink="true">https://martianov.dev/learn-about-scalas-case-classes-and-built-in-extractor-functions</guid><category><![CDATA[Scala]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Thu, 10 Aug 2023 20:37:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Bgoceb_kc0k/upload/e1bc0ecdda5f194dd7fc5c2cdcafd3cc.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Case classes in Scala are very handy because they have build extractor capability and other free features.</p>
<p>We can define a case class and then use it conveniently in the code:</p>
<pre><code class="lang-scala"><span class="hljs-keyword">import</span> scala.collection.mutable

<span class="hljs-keyword">case</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyFeature</span>(<span class="hljs-params">a: <span class="hljs-type">Int</span>, b: <span class="hljs-type">Int</span>, c: <span class="hljs-type">Int</span>, d: <span class="hljs-type">Int</span></span>)</span>

<span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">Solution</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">App</span> </span>{
  <span class="hljs-keyword">val</span> feature = <span class="hljs-type">MyFeature</span>(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>)
  <span class="hljs-keyword">val</span> queue = mutable.<span class="hljs-type">Queue</span>.empty[<span class="hljs-type">MyFeature</span>]
  queue.enqueue(feature)
  <span class="hljs-keyword">while</span>(queue.nonEmpty) {
    queue.dequeue() <span class="hljs-keyword">match</span> {
      <span class="hljs-keyword">case</span> <span class="hljs-type">MyFeature</span>(_,b,_,_) =&gt; <span class="hljs-comment">// do something with B</span>
    }
  }
}
</code></pre>
<p>The problem starts when we have to deal with model evolution. What if we need to add a new parameter to MyFeature and update the case class definition to MyFeature(a: Int, b: Int, c: Int, d: Int<strong>, e: Int</strong>)</p>
<p>If we launch our code as is, Scala will start to complain:</p>
<blockquote>
<p>Wrong number of arguments for the extractor, found: 4, expected: 5</p>
</blockquote>
<p>It means that you will have to find all places in the codebase where the built-in MyFeature extractor has been used and fix all of them.</p>
<pre><code class="lang-scala"><span class="hljs-keyword">case</span> <span class="hljs-type">MyFeature</span>(_,b,_,_,e) =&gt; ...
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If you expect a lot of extensions of your Scala case class, it is better to make a custom extractor instead of using a build-in extractor</div>
</div>

<p>First, let's implement a custom extractor</p>
<pre><code class="lang-scala"><span class="hljs-class"><span class="hljs-keyword">object</span> <span class="hljs-title">BParameter</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">unapply</span></span>(f: <span class="hljs-type">MyFeature</span>): <span class="hljs-type">Option</span>[<span class="hljs-type">Int</span>] = {
    <span class="hljs-type">Some</span>(f.b)
  }
}
</code></pre>
<p>Refactor the original loop with our new extractor</p>
<pre><code class="lang-scala"><span class="hljs-keyword">while</span>(queue.nonEmpty) {
  queue.dequeue() <span class="hljs-keyword">match</span> {
    <span class="hljs-keyword">case</span> <span class="hljs-type">BParameter</span>(b) =&gt; <span class="hljs-comment">// do something with B</span>
  }
}
</code></pre>
<p>This code will not depend on MyFeature extensions. Additionally, you can define many unapply methods with different parameters to extract the same type of objects from different classes.</p>
<p>In conclusion, while Scala's case classes provide built-in extractors for convenience, they can become problematic when dealing with model evolution. To avoid issues, consider implementing custom extractors, which offer more flexibility and adaptability as your case class evolves.</p>
]]></content:encoded></item></channel></rss>