<?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>Sat, 16 May 2026 05:18:10 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[Splitting the Brain: Plugin vs LSP]]></title><description><![CDATA[The GALA IDE experience runs on two processes. One is a Kotlin plugin inside IntelliJ. The other is a Go binary speaking LSP over stdio. They share a grammar, they share a user, and they disagree about almost everything else. This post explains why t...]]></description><link>https://martianov.dev/splitting-the-brain-plugin-vs-lsp</link><guid isPermaLink="true">https://martianov.dev/splitting-the-brain-plugin-vs-lsp</guid><category><![CDATA[compilers]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Tue, 14 Apr 2026 02:09:08 GMT</pubDate><content:encoded><![CDATA[<p>The GALA IDE experience runs on two processes. One is a Kotlin plugin inside IntelliJ. The other is a Go binary speaking LSP over stdio. They share a grammar, they share a user, and they disagree about almost everything else. This post explains why the split exists, what each side owns, and the synchronization contract that keeps them from drifting apart.</p>
<h2 id="heading-why-not-put-everything-in-one-place">Why Not Put Everything in One Place?</h2>
<p>The obvious question: why not put all IDE features in the LSP server? Or all of them in the plugin?</p>
<p><strong>Everything in the LSP</strong> sounds right until you type a character. The LSP server runs the full GALA transpilation pipeline -- parse, analyze, transform -- to produce type information. That pipeline takes tens to hundreds of milliseconds depending on file size and import graph depth. Syntax highlighting, brace matching, and code folding need to respond in under 5ms or the editor feels broken. You cannot debounce syntax highlighting. It has to be synchronous and local.</p>
<p><strong>Everything in the plugin</strong> sounds right until you need types. The IntelliJ plugin has a PSI tree built from the ANTLR grammar. It knows the structure of the code -- where functions are, where braces match, what tokens are keywords. But it has no idea what type <code>x</code> is in <code>val x = people.Map((p) =&gt; p.Name)</code>. That requires running the analyzer, resolving imports across packages, and executing the transformer's type inference. Reimplementing that in Kotlin would mean maintaining two type systems, and the transpiler's type system is the only one that matters because it produces the Go output.</p>
<p>So the split is forced by latency on one side and semantic depth on the other.</p>
<h2 id="heading-what-the-plugin-owns">What the Plugin Owns</h2>
<p>The plugin handles everything that can be answered by looking at tokens and the local PSI tree, without running the transpiler.</p>
<p><strong>Syntax highlighting.</strong> The ANTLR grammar generates a Java lexer (Bazel patches <code>-&gt; skip</code> to <code>-&gt; channel(HIDDEN)</code> so whitespace tokens survive for the IDE). <code>GalaSyntaxHighlighter.kt</code> maps token types to IntelliJ's text attribute keys -- keywords get one color, string literals another, comments another. This is pure token classification; no parsing required.</p>
<p><strong>Brace matching.</strong> Parentheses, braces, brackets. The plugin registers <code>GalaBraceMatcher</code> and IntelliJ handles the rest. Zero network calls.</p>
<p><strong>Code folding.</strong> <code>GalaFoldingBuilder.kt</code> walks the PSI tree looking for block nodes (function bodies, sealed type declarations, import groups) and registers foldable regions. This is structural, not semantic -- you don't need to know what a function returns to know where its body ends.</p>
<p><strong>Structure view.</strong> <code>GalaStructureViewElement.kt</code> extracts top-level declarations from the PSI tree -- functions, types, sealed types with their cases, vals, vars. This powers the sidebar outline and Ctrl+F12 navigation. Again, purely structural.</p>
<p><strong>Keyword and basic completion.</strong> <code>GalaCompletionContributor.kt</code> offers keyword completions (<code>val</code>, <code>func</code>, <code>match</code>, <code>sealed</code>) and built-in type names (<code>int</code>, <code>string</code>, <code>bool</code>, <code>Option</code>, <code>Either</code>). These are hardcoded lists. They respond instantly because there is nothing to compute.</p>
<p><strong>Live templates.</strong> Snippet expansion for common patterns. Local, instant, no server involved.</p>
<p>The common trait: all of these features are fast because they operate on data the plugin already has. No IPC, no analysis, no waiting.</p>
<h2 id="heading-what-the-lsp-server-owns">What the LSP Server Owns</h2>
<p>The LSP server handles everything that requires the transpiler's semantic model. The <code>GalaHandler</code> in <code>server.go</code> wraps the full pipeline: ANTLR parser, analyzer, and transformer.</p>
<p><strong>Diagnostics.</strong> On every edit, the server runs <code>analyzeFile</code> -- parse the source, run the analyzer to resolve imports and build the <code>RichAST</code>, then run the transformer via <code>TransformForLSP</code> to catch type errors. Parse errors, semantic errors, and match exhaustiveness warnings all flow back as LSP diagnostics. The server uses a cancel-and-restart pattern with a 300ms debounce: each keystroke cancels any in-flight analysis and starts a new timer. Before publishing, it checks the document version to discard stale results. Old diagnostics stay visible until replaced -- no flicker.</p>
<p><strong>Type-aware completion.</strong> When the user types a dot, <code>completion.go</code> resolves the receiver's type using the cached <code>RichAST</code> and the transformer's <code>VarTypes</code> map. If you type <code>person.</code>, the server looks up the type of <code>person</code>, finds its methods and fields in <code>richAST.Types</code>, and returns them. This is qualitatively different from the plugin's keyword completion -- it knows that <code>person</code> is a <code>Person</code> struct with a <code>Name</code> field and an <code>Age</code> field. It also handles package-qualified dot completion (<code>collection_immutable.Array</code>), named argument completion inside constructors (<code>Person(Name =</code>), and match case completion with field destructuring for sealed types.</p>
<p><strong>Inlay hints.</strong> For <code>val x = someExpression</code>, the server resolves the type of <code>someExpression</code> through the transformer and displays the inferred type inline. This requires the full type inference pipeline.</p>
<p><strong>Hover.</strong> Ctrl+Q shows type signatures, struct fields, method lists, and sealed type variants. All sourced from the <code>RichAST</code>.</p>
<p><strong>Go-to-definition.</strong> Cross-file navigation using the analyzer's resolved metadata. The plugin's PSI tree only knows about the current file; the analyzer knows the entire package graph.</p>
<h2 id="heading-the-sync-problem">The Sync Problem</h2>
<p>Here is where it gets uncomfortable. The plugin has hardcoded lists:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// GalaCompletionContributor.kt</span>
<span class="hljs-keyword">val</span> DECLARATION_KEYWORDS = listOf(<span class="hljs-string">"val"</span>, <span class="hljs-string">"var"</span>, <span class="hljs-string">"func"</span>, <span class="hljs-string">"type"</span>, <span class="hljs-string">"struct"</span>, ...)
<span class="hljs-keyword">val</span> BUILTIN_TYPES = listOf(<span class="hljs-string">"int"</span>, <span class="hljs-string">"string"</span>, <span class="hljs-string">"bool"</span>, <span class="hljs-string">"float64"</span>, <span class="hljs-string">"Option"</span>, <span class="hljs-string">"Either"</span>, ...)
</code></pre>
<p>The LSP server has its own list:</p>
<pre><code class="lang-go"><span class="hljs-comment">// completion.go</span>
keywords := []<span class="hljs-keyword">string</span>{
    <span class="hljs-string">"package"</span>, <span class="hljs-string">"import"</span>, <span class="hljs-string">"val"</span>, <span class="hljs-string">"var"</span>, <span class="hljs-string">"func"</span>, <span class="hljs-string">"type"</span>, <span class="hljs-string">"struct"</span>,
    <span class="hljs-string">"interface"</span>, <span class="hljs-string">"sealed"</span>, <span class="hljs-string">"embed"</span>, <span class="hljs-string">"if"</span>, <span class="hljs-string">"else"</span>, <span class="hljs-string">"for"</span>, <span class="hljs-string">"range"</span>,
    <span class="hljs-string">"return"</span>, <span class="hljs-string">"match"</span>, <span class="hljs-string">"case"</span>, <span class="hljs-string">"true"</span>, <span class="hljs-string">"false"</span>, <span class="hljs-string">"nil"</span>, <span class="hljs-string">"map"</span>,
}
</code></pre>
<p>And the actual source of truth is neither of these -- it is the ANTLR grammar (<code>gala.g4</code>) for keywords and <code>types.go</code>'s <code>IsPrimitiveType()</code> for built-in types.</p>
<p>If someone adds a keyword to the grammar and forgets to update the plugin, keyword completion silently omits it. If someone adds a built-in type to the transpiler and forgets to update the syntax highlighter, that type name renders in the wrong color. These bugs are subtle, they don't cause test failures, and they erode trust in the tooling.</p>
<h2 id="heading-syncmd-as-a-contract">SYNC.md as a Contract</h2>
<p>GALA's answer is a synchronization document (<code>ide/intellij/SYNC.md</code>) that explicitly lists every hardcoded data point, its authoritative source, and how to extract the current values. Nine sync points are documented:</p>
<ol>
<li><strong>Keywords</strong> -- plugin completion lists and highlighter vs. <code>gala.g4</code> lexer rules</li>
<li><strong>Built-in types</strong> -- plugin annotator, highlighter, and completion vs. <code>types.go</code></li>
<li><strong>Std auto-imported types</strong> -- plugin completion vs. <code>std/*.gala</code> sealed types</li>
<li><strong>Std methods</strong> -- plugin postfix templates vs. <code>std/*.gala</code> method definitions</li>
<li><strong>Importable package types</strong> -- explicitly marked as NOT in static lists (LSP-only)</li>
<li><strong>Highlighter token mapping</strong> -- plugin <code>when</code> block vs. generated Java lexer constants</li>
<li><strong>LSP completion keywords</strong> -- <code>completion.go</code> vs. <code>gala.g4</code></li>
<li><strong>LSP type inference</strong> -- <code>inlay_hints.go</code> vs. transpiler type system</li>
<li><strong>LSP match exhaustiveness</strong> -- reads from <code>RichAST</code> at runtime (no hardcoded data)</li>
</ol>
<p>Each entry includes the exact file paths and shell commands to extract current values. The document also specifies trigger conditions: sync after any grammar change, new std type, new collection type, or built-in type change.</p>
<p>This is not automated enforcement -- it is a documented contract. When you change the grammar, you know exactly which files to update because the document tells you. It trades perfection for pragmatism: a grep command you can run is more useful than a validation framework nobody maintains.</p>
<h2 id="heading-trade-offs-in-practice">Trade-offs in Practice</h2>
<p><strong>Latency.</strong> The plugin responds in under 5ms for highlighting, folding, brace matching, and keyword completion. The LSP server debounces at 300ms and analysis can take longer. Users see instant syntax feedback but slightly delayed error squiggles. This is the right trade-off -- laggy highlighting is unacceptable, laggy diagnostics are tolerable.</p>
<p><strong>Accuracy.</strong> The plugin's keyword completion will suggest <code>match</code> whether or not you are in a context where <code>match</code> is valid. The LSP server's type-aware completion will only suggest methods that actually exist on the receiver's type. Local is approximate; semantic is precise.</p>
<p><strong>Portability.</strong> The IntelliJ plugin works only in JetBrains IDEs. The LSP server works in VS Code, Neovim, Helix -- anything that speaks LSP 3.17 over stdio. If GALA only had the plugin, two-thirds of potential users would have no IDE support. The LSP server is the portable foundation; the plugin is the premium layer for JetBrains users.</p>
<p><strong>Maintenance cost.</strong> Two codebases in two languages (Kotlin and Go) with shared assumptions. Every grammar change touches both. The SYNC.md contract reduces the risk but does not eliminate it. This is the real cost of the split architecture, and it is worth paying because the alternative -- reimplementing type inference in Kotlin or accepting 300ms syntax highlighting latency -- is worse.</p>
<h2 id="heading-the-uncomfortable-truth">The Uncomfortable Truth</h2>
<p>A two-process architecture is not elegant. It is a pragmatic response to competing constraints: the plugin must be fast, the server must be smart, and neither can do the other's job well. The synchronization contract is a maintenance burden that scales linearly with language complexity. Every new keyword, every new built-in type, every new sealed variant in std is a potential sync failure.</p>
<p>But the alternative architectures are worse. A pure-LSP approach makes the editor feel sluggish. A pure-plugin approach cannot provide semantic features without reimplementing the transpiler. A single-binary approach that embeds the transpiler in a JetBrains plugin locks out every other editor.</p>
<p>The split is the least bad option. SYNC.md is the acknowledgment that it has costs. And the 300ms debounce in <code>DidChange</code> is the number that makes it all work -- fast enough to feel responsive, slow enough to avoid burning CPU on every keystroke.</p>
]]></content:encoded></item><item><title><![CDATA[Dot Completion Without a Type Checker]]></title><description><![CDATA[The user types order. and pauses. The editor fires a textDocument/completion request. The LSP has about 100 milliseconds to figure out what order is, what methods and fields it has, and return a sorted list of suggestions with signatures. Most langua...]]></description><link>https://martianov.dev/dot-completion-without-a-type-checker</link><guid isPermaLink="true">https://martianov.dev/dot-completion-without-a-type-checker</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>Sat, 11 Apr 2026 02:06:53 GMT</pubDate><content:encoded><![CDATA[<p>The user types <code>order.</code> and pauses. The editor fires a <code>textDocument/completion</code> request. The LSP has about 100 milliseconds to figure out what <code>order</code> is, what methods and fields it has, and return a sorted list of suggestions with signatures. Most language servers delegate this to a type checker -- a standalone component that maintains a typed AST of the whole program. GALA doesn't have one. It has a transpiler that targets Go.</p>
<p>This post is about how we fake it.</p>
<h2 id="heading-the-completion-cascade">The completion cascade</h2>
<p>Before resolving any types, the handler needs to figure out <em>what kind</em> of completion the user wants. GALA's completion handler runs a context detection cascade -- four checks in priority order:</p>
<pre><code class="lang-go">isDot := isDotCompletion(text, line, char)

<span class="hljs-keyword">if</span> isDot &amp;&amp; richAST != <span class="hljs-literal">nil</span> {
    receiverType := typeAtDot(text, line, char, richAST, varTypeMap)
    <span class="hljs-keyword">if</span> strings.HasPrefix(receiverType, packagePrefix) {
        pkgName := strings.TrimPrefix(receiverType, packagePrefix)
        items = <span class="hljs-built_in">append</span>(items, packageCompletions(richAST, pkgName)...)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> receiverType != <span class="hljs-string">""</span> {
        items = <span class="hljs-built_in">append</span>(items, typeSpecificCompletions(richAST, receiverType)...)
    }
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> isNamedArgContext(text, line, char) &amp;&amp; richAST != <span class="hljs-literal">nil</span> {
    <span class="hljs-comment">// Constructor named args: Person(name = ...)</span>
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> isMatchCaseContext(text, line, char) &amp;&amp; richAST != <span class="hljs-literal">nil</span> {
    <span class="hljs-comment">// Sealed type case suggestions</span>
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Global: types, functions, keywords</span>
}
</code></pre>
<p>Dot completion wins. If it fires, nothing else runs. This matters because a dot inside a constructor call (<code>Person(address = city.</code>) is both a named-arg context and a dot context. The dot takes priority because the user is actively accessing a member.</p>
<h2 id="heading-detecting-the-dot">Detecting the dot</h2>
<p><code>isDotCompletion</code> does something deceptively simple: it walks backwards from the cursor position on the current line, looking for a <code>.</code> character. But there's a subtlety. When the user types <code>order.ge</code>, the cursor is after <code>e</code>, not after <code>.</code>. The partial identifier <code>ge</code> is what they've typed so far. We need to skip past it:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">isDotCompletion</span><span class="hljs-params">(text <span class="hljs-keyword">string</span>, line, char <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span></span> {
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">if</span> l[char<span class="hljs-number">-1</span>] == <span class="hljs-string">'.'</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
    <span class="hljs-comment">// Walk backwards past any partial identifier being typed</span>
    i := char - <span class="hljs-number">1</span>
    <span class="hljs-keyword">for</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; isIdentChar(l[i]) {
        i--
    }
    <span class="hljs-keyword">if</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; l[i] == <span class="hljs-string">'.'</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}
</code></pre>
<p>This handles both <code>order.</code> (cursor right after dot) and <code>order.ge</code> (cursor mid-identifier after dot). The partial identifier is irrelevant for type resolution -- we only care about what's to the left of the dot.</p>
<h2 id="heading-resolving-the-receiver-type">Resolving the receiver type</h2>
<p>This is where things get interesting. <code>typeAtDot</code> needs to figure out what type <code>order</code> is. It has no type checker, no symbol table built from a full program analysis. What it has:</p>
<ol>
<li><p><strong>VarTypes</strong> -- a <code>map[string]string</code> populated by the transpiler during its last successful transform. Keys are scoped: <code>"main.order"</code> maps to <code>"Order"</code>, <code>"processItems.item"</code> maps to <code>"Item"</code>. The transpiler already resolved these types while generating Go code; we just reuse its work.</p>
</li>
<li><p><strong>RichAST</strong> -- the transpiler's analysis output containing type metadata (fields, methods, sealed variants), function signatures, package information, and import aliases.</p>
</li>
</ol>
<p>The resolution cascade in <code>resolveReceiverType</code> tries five strategies in order:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">resolveReceiverType</span><span class="hljs-params">(name, funcScope <span class="hljs-keyword">string</span>, richAST *transpiler.RichAST,
    varTypes <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-comment">// 1. Transpiler's resolved var types (function-scoped)</span>
    <span class="hljs-keyword">if</span> typStr := lookupVarType(varTypes, funcScope, name); typStr != <span class="hljs-string">""</span> {
        <span class="hljs-keyword">return</span> stripTypeParams(typStr)
    }
    <span class="hljs-comment">// 2. Function call return type: GetOrder() → Order</span>
    <span class="hljs-keyword">if</span> fm, ok := richAST.Functions[name]; ok { ... }
    <span class="hljs-comment">// 3. Sealed case constructor → parent type: Some(42) → Option</span>
    <span class="hljs-keyword">for</span> _, tm := <span class="hljs-keyword">range</span> richAST.Types { ... }
    <span class="hljs-comment">// 4. Package name → package marker</span>
    <span class="hljs-keyword">for</span> _, pkgName := <span class="hljs-keyword">range</span> richAST.Packages { ... }
    <span class="hljs-comment">// 5. Type name for static calls: List.Empty()</span>
    <span class="hljs-keyword">if</span> isExported(name) { <span class="hljs-keyword">if</span> findType(richAST, name) != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> name } }
    <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>
}
</code></pre>
<p>The scoped lookup is important. <code>lookupVarType</code> tries <code>funcScope + "." + varName</code> first, then falls back to an unscoped lookup. If you have a <code>val order = ...</code> inside <code>func main</code>, it's stored as <code>"main.order"</code>. The enclosing function is found by scanning backwards from the cursor for a <code>func</code> declaration -- crude, but effective for GALA's single-function-per-block structure.</p>
<h2 id="heading-the-chain-problem">The chain problem</h2>
<p>Simple variable access is easy. The hard case is chains: <code>collection.Map(transform).Filter(predicate).</code>. The user expects completions for whatever <code>Filter</code> returns, which means we need to resolve the entire chain left-to-right.</p>
<p><code>typeAtDot</code> handles this by detecting a closing paren before the dot. When it sees <code>)</code>, it walks backwards through balanced parentheses to find the method name, then checks if there's another dot before that name -- indicating a chain:</p>
<pre><code class="lang-go"><span class="hljs-comment">// Case 1: Closing paren before dot — expr.Method().</span>
<span class="hljs-keyword">if</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; l[i] == <span class="hljs-string">')'</span> {
    depth := <span class="hljs-number">1</span>
    i--
    <span class="hljs-keyword">for</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; depth &gt; <span class="hljs-number">0</span> {
        <span class="hljs-keyword">if</span> l[i] == <span class="hljs-string">')'</span> { depth++ }
        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> l[i] == <span class="hljs-string">'('</span> { depth-- }
        i--
    }
    <span class="hljs-comment">// Extract the method name before '('</span>
    name := l[nameStart:nameEnd]
    <span class="hljs-keyword">if</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; l[i] == <span class="hljs-string">'.'</span> {
        <span class="hljs-comment">// Chained: resolve receiver, then get method's return type</span>
        receiverType := resolveChainType(l[:i], enclosingFunc, richAST, varTypes)
        <span class="hljs-keyword">if</span> receiverType != <span class="hljs-string">""</span> {
            <span class="hljs-keyword">return</span> resolveMethodReturn(richAST, receiverType, name)
        }
    }
}
</code></pre>
<p><code>resolveChainType</code> is recursive. It peels off the rightmost segment of the expression, resolves its type, then uses <code>resolveMethodReturn</code> to look up what that method returns on that type. A depth limit of 10 prevents infinite recursion on pathological inputs:</p>
<pre><code class="lang-go"><span class="hljs-keyword">const</span> maxChainDepth = <span class="hljs-number">10</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">resolveChainTypeN</span><span class="hljs-params">(text <span class="hljs-keyword">string</span>, funcScope <span class="hljs-keyword">string</span>, richAST *transpiler.RichAST,
    varTypes <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>, depth <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">if</span> depth &gt; maxChainDepth { <span class="hljs-keyword">return</span> <span class="hljs-string">""</span> }
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">if</span> text[<span class="hljs-built_in">len</span>(text)<span class="hljs-number">-1</span>] == <span class="hljs-string">')'</span> {
        <span class="hljs-comment">// Walk balanced parens, extract method name</span>
        <span class="hljs-keyword">if</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; text[i] == <span class="hljs-string">'.'</span> {
            receiverType := resolveChainTypeN(text[:i], funcScope, richAST, varTypes, depth+<span class="hljs-number">1</span>)
            <span class="hljs-keyword">if</span> receiverType != <span class="hljs-string">""</span> {
                <span class="hljs-keyword">return</span> resolveMethodReturn(richAST, receiverType, methodName)
            }
        }
    }
    <span class="hljs-comment">// Plain identifier — resolve directly</span>
    <span class="hljs-keyword">return</span> resolveReceiverType(name, funcScope, richAST, varTypes)
}
</code></pre>
<p>So for <code>collection.Map(transform).Filter(predicate).</code>, resolution proceeds: resolve <code>collection</code> from VarTypes (say, <code>List</code>), look up <code>Map</code>'s return type on <code>List</code> (say, <code>List</code>), look up <code>Filter</code>'s return type on <code>List</code> (say, <code>List</code>), then show <code>List</code>'s methods.</p>
<p>There's an obvious limitation: generic type parameters are erased. <code>stripTypeParams</code> drops <code>[T]</code> from <code>List[int]</code> before lookup, so <code>collection.Map(toStr).</code> still shows <code>List</code> methods, not <code>List[string]</code> methods. The method set is the same either way for GALA's standard library types, so this works in practice. It wouldn't work for a language where different type parameters produce different method sets.</p>
<h2 id="heading-the-package-hack">The <code>__package__</code> hack</h2>
<p>When the user types <code>http.</code>, they're not accessing a field on a variable -- they want the exports of the <code>http</code> package. But the resolution cascade would happily try to find a variable called <code>http</code> in VarTypes, fail, and return nothing.</p>
<p>The fix is a sentinel value. <code>resolveReceiverType</code> checks the RichAST's package list and import aliases. If the name matches a known package, it returns <code>"__package__:http"</code> instead of a type name:</p>
<pre><code class="lang-go"><span class="hljs-keyword">const</span> packagePrefix = <span class="hljs-string">"__package__:"</span>

<span class="hljs-comment">// In resolveReceiverType:</span>
<span class="hljs-keyword">for</span> _, pkgName := <span class="hljs-keyword">range</span> richAST.Packages {
    <span class="hljs-keyword">if</span> pkgName == name {
        <span class="hljs-keyword">return</span> packagePrefix + name
    }
}
<span class="hljs-keyword">if</span> richAST.ImportAliases != <span class="hljs-literal">nil</span> {
    <span class="hljs-keyword">if</span> pkgName, ok := richAST.ImportAliases[name]; ok {
        <span class="hljs-keyword">return</span> packagePrefix + pkgName
    }
}
</code></pre>
<p>Back in the completion handler, this prefix triggers a completely different code path -- <code>packageCompletions</code> instead of <code>typeSpecificCompletions</code>. Package completions pull from three sources: types belonging to that package, functions belonging to that package, and <code>GoExports</code> (a map of raw export names from Go-only packages that the transpiler has seen but not fully analyzed).</p>
<p>The alias handling matters. If the GALA source has <code>import io = "collection/immutable"</code>, typing <code>io.</code> needs to resolve the alias <code>io</code> to the actual package name <code>collection/immutable</code> and show its exports.</p>
<h2 id="heading-generating-completions">Generating completions</h2>
<p>Once we have a resolved type name, <code>typeSpecificCompletions</code> looks it up in the RichAST via <code>findType</code> and builds completion items from three sources:</p>
<p><strong>Methods</strong> get full signatures and smart insert text. Zero-parameter methods insert <code>()</code> immediately; methods with parameters insert <code>(</code> and let the editor handle the rest (which sets up signature help):</p>
<pre><code class="lang-go"><span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(m.ParamNames) == <span class="hljs-number">0</span> {
    insertText = name + <span class="hljs-string">"()"</span>
} <span class="hljs-keyword">else</span> {
    insertText = name + <span class="hljs-string">"("</span>
}
</code></pre>
<p><strong>Fields</strong> show their type as the detail string. <strong>Sealed variants</strong> generate <code>IsXxx()</code> predicate methods. And every type gets a <code>match</code> keyword suggestion, because pattern matching is idiomatic in GALA.</p>
<p><code>findType</code> itself is more resilient than you'd expect. It tries direct key lookup, <code>std.</code>-prefixed lookup (GALA's standard library), qualified-name decomposition (<code>mylib.Animal</code> matches a type with simple name <code>Animal</code>), and type alias resolution. This layered lookup handles the fact that the RichAST keys aren't always consistent -- sometimes they're package-qualified, sometimes they're not, depending on how the type was declared.</p>
<h2 id="heading-edge-cases-cursor-position-semantics">Edge cases: cursor position semantics</h2>
<p>Different editors report cursor positions differently when the user triggers completion on a dot. Some place the cursor <em>on</em> the dot (before it), others place it <em>after</em> the dot. VS Code places it after, which is what <code>isDotCompletion</code> checks first (<code>l[char-1] == '.'</code>). But when the user has typed <code>order.ge</code> and triggers completion mid-word, the cursor is past both the dot and the partial identifier. The backwards walk through <code>isIdentChar</code> handles this uniformly.</p>
<p>There's also the question of what happens when the dot is at column 0 (nothing before it) or when the line itself is shorter than the reported cursor position. Both are guarded: <code>char &lt;= 0</code> returns early, and <code>char</code> is clamped to <code>len(l)</code> to handle editors that report positions beyond line length.</p>
<h2 id="heading-what-were-actually-doing">What we're actually doing</h2>
<p>The core insight is that we never build a type checker. We mooch off the transpiler. Every time the document changes, the LSP runs <code>TransformForLSP</code>, which does a partial transpilation and dumps its resolved variable types into a flat map. The LSP's job is just text surgery -- walk backwards from the cursor, figure out what syntactic construct we're in, and look up the answer in pre-computed tables.</p>
<p>It's fragile in ways a real type checker wouldn't be. Chains longer than what the transpiler resolved break. Generic type parameters are erased. Completions after a syntax error on the same line might fail because the backwards paren-walk gets confused. But it's fast, it handles the common cases, and it was built in a fraction of the time a real type checker would take.</p>
<p>Sometimes the right engineering decision is to not build the thing you think you need.</p>
]]></content:encoded></item><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>