<?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, 05 Sep 2026 02:23:29 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[Data Races Are a Compile Error Now: GALA in July (0.63 -> 0.72)]]></title><description><![CDATA[Go's race detector is very good at what it does. What it does is watch a program
run and report races it actually observed.
That last part is the whole problem. go test -race finds the race your test
]]></description><link>https://martianov.dev/gala-july-2026-data-race-safety</link><guid isPermaLink="true">https://martianov.dev/gala-july-2026-data-race-safety</guid><category><![CDATA[Go Language]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[compiler]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Tue, 28 Jul 2026 03:43:21 GMT</pubDate><content:encoded><![CDATA[<p>Go's race detector is very good at what it does. What it does is watch a program
run and report races it actually observed.</p>
<p>That last part is the whole problem. <code>go test -race</code> finds the race your test
happened to schedule, on the interleaving it happened to get, on the machine it
happened to run on. It is a sampling instrument pointed at a nondeterministic
process. The races it misses are not exotic ones — they are the ordinary ones
that needed a slightly different timing to show up, which is also a fair
description of the races that reach production.</p>
<p>Seven releases have landed since <a href="https://martianov.dev/gala-june-2026-bind-also">the last of these
posts</a>, 0.63 through 0.72.2. The
one that changes how the language feels is this: <strong>a value that isn't safe to
share across goroutines is now a compile error.</strong> Not a warning, not a lint, not
a runtime sampler. A build failure, with a caret pointing at the capture.</p>
<hr />
<h2>The rule</h2>
<p>A data race needs two goroutines touching the same memory with at least one
writing. The cleanest way to make that impossible is to guarantee the value is
<strong>deeply immutable</strong>: if nobody can ever mutate it, any number of goroutines can
read it concurrently with no synchronisation and no race.</p>
<p>So GALA's rule for crossing a goroutine boundary is exactly one sentence:</p>
<blockquote>
<p>Only deeply-immutable values may cross.</p>
</blockquote>
<p>The interesting question is not whether that rule is sound — it obviously is —
but whether a language can adopt it without making concurrent code miserable to
write. In most languages this rule would be intolerable, because most values in
most programs are mutable.</p>
<p>GALA can afford it because the language already pushed everything the other way:
<code>val</code> over <code>var</code>, <code>collection_immutable</code> over <code>collection_mutable</code>,
<code>Option</code>/<code>Try</code>/<code>Either</code> over <code>nil</code> and naked error pairs, immutable struct fields
by default. Most values a well-written GALA program shares are <em>already</em>
shareable. The check is therefore silent in the common case. It only speaks up
when you try to send something genuinely mutable.</p>
<p>That's the design bet: the safety check is cheap because the language spent
three years making immutability the path of least resistance.</p>
<hr />
<h2>What it looks like when it fires</h2>
<p>A concurrency boundary is a parameter typed <code>Sendable[F]</code>. <code>concurrent.Future</code>
uses one; so does <code>FutureOn</code>; so can your own library. Here is a boundary and a
closure that captures a <code>var</code>:</p>
<pre><code class="language-gala">package main

func run(body Sendable[func() int]) int = body()

func main() {
    var counter = 0
    Println(run(() =&gt; counter + 1))
}
</code></pre>
<p>That program does not compile:</p>
<pre><code>error[GALA-E0037]: closure crossing a concurrency boundary captures reassignable var "counter" — a data race, because the enclosing scope may reassign it while the goroutine runs
  --&gt; var_capture.gala:7:23
  |
7 |     Println(run(() =&gt; counter + 1))
  |                       ^^^^^^^ snapshot it into an immutable `val` before the boundary
  |
  = hint: snapshot it into an immutable `val` before the boundary (e.g. `val counter = counter` outside the closure) so the goroutine captures a stable copy
</code></pre>
<p>The closure never ran. No scheduler had to cooperate. There is no flaky-test
phase where this reproduces on CI one time in forty.</p>
<p>Four shapes trigger <code>GALA-E0037</code>: capturing a reassignable <code>var</code>; using a <code>val</code>
<em>whole</em> when its type isn't deeply immutable (a <code>collection_mutable</code> value, a
struct with a <code>var</code> field, a Go-interop handle, a bare slice/map/pointer);
reading a field path that passes through a <code>var</code> field; and capturing a bare
<code>func</code> value, whose type says nothing about what <em>it</em> closed over.</p>
<p>That last one matters more than it looks. A <code>func() int</code> parameter is an opaque
promise — you cannot see its captures, so you cannot vouch for them. Typing the
parameter <code>Sendable[func() int]</code> is how the guarantee propagates: the caller
vouches, and the callee may forward it into a further boundary.</p>
<hr />
<h2>What counts as shareable</h2>
<p>The predicate is deep, transitive, and structural. It looks through wrappers,
collections, tuples, struct fields, and sealed variants until it hits a
primitive or something it must reject.</p>
<p>Shareable: the value primitives; <code>string</code>; every <code>collection_immutable</code>
structure (<code>Array</code>, <code>List</code>, <code>HashMap</code>, <code>HashSet</code>, <code>TreeMap</code>, <code>TreeSet</code>) when
their type arguments are; <code>Option</code>/<code>Try</code>/<code>Either</code> and tuples when theirs are;
immutable structs and sealed types all the way down.</p>
<p>Not shareable: anything from <code>collection_mutable</code>, structs with a <code>var</code> field,
raw Go slices, maps and pointers, and opaque Go-interop references.</p>
<p>Which means this compiles, and every capture in it is checked:</p>
<pre><code class="language-gala">val xs = ArrayOf(10, 20, 30)
val f = Future(xs.Size())
Println(f.Get())
</code></pre>
<p><code>Future(expr)</code> is by-name thunk sugar — it desugars to <code>Future(() =&gt; expr)</code>, so
<code>expr</code> <em>is</em> the async closure, and its captures get the same treatment as an
explicit lambda.</p>
<hr />
<h2>Field sensitivity, and the limit that's actually left</h2>
<p>A whole-value check would be annoying enough to route around. If sharing a
struct required the struct to be immutable in its entirety, every <code>Future</code> would
open with a stanza of snapshot locals.</p>
<p>So the check is <strong>field-access-sensitive</strong>. It records <em>how</em> the closure uses
each capture, and a capture read only through immutable field paths is accepted
even when the struct as a whole is not shareable:</p>
<pre><code class="language-gala">struct AppModel(team string, statuses Array[int], var attempts int)

val model = AppModel("qa", ArrayOf(1, 2, 3), 0)

// ACCEPTED — reads only the immutable `team` / `statuses` fields, even though
// AppModel is unshareable as a whole (it has a `var attempts` field).
Future(() =&gt; project(model.team, model.statuses))

// REJECTED — reads through the `var` field.
Future(() =&gt; model.attempts + 1)
</code></pre>
<p>Calling a method on a field you just read works too. The receiver's own type
decides, so the common shapes real code writes are accepted:</p>
<pre><code class="language-gala">// ACCEPTED — `team` is a `string`, unconditionally shareable.
Future(() =&gt; model.team.Size())

// REJECTED — the receiver field's own type is mutable.
Future(() =&gt; model.buf.Size())    // buf: collection_mutable.Array[int]
</code></pre>
<p>That last part is newer than the rest of this post. When I first wrote it up,
<code>model.team.Size()</code> was rejected — a method call ended the field-path run and
collapsed the whole capture, so a <code>string</code> that could never race got refused. It
was sound but wrong-feeling, and it landed on a shape you write constantly
(<code>model.name.Size()</code>, <code>model.items.Map(...)</code>). It's fixed in 0.72.1: a trailing
method call on a field path defers to the leaf's shareability. Indexing and
chained calls stay conservative and still mark the capture whole.</p>
<p>Which is roughly what you want from a check this young — the sound-but-annoying
direction is the recoverable one, and it got recovered in a point release.</p>
<p>One more scope limit worth stating plainly: this is <strong>not</strong> a general race
detector. It guards declared <code>Sendable[F]</code> boundaries. Reach around them — hand
a pointer to hand-written Go, share state through a Go library — and you are
back to <code>go test -race</code>, exactly as before. It makes the safe path checkable, it
does not make the unsafe path impossible.</p>
<hr />
<h2>The rest of the month</h2>
<p><strong>Structured concurrency and cancellation.</strong> <code>Future</code> gained real cancellation
with scope semantics: linear combinators inherit their parent's token, so a
derived stage shares its scope, while a fresh root opens a new one. Cancellation
is API-level — there's no body-token escape hatch to misuse.</p>
<p><strong>Stack traces name your <code>.gala</code> lines.</strong> A panic now reports GALA source
positions, not positions in generated Go:</p>
<pre><code>panic: runtime error: integer divide by zero
main.divide(...)
	foo.gala:4
main.main()
	foo.gala:8
</code></pre>
<p>This rides Go's own <code>//line</code> mechanism — the transpiler stamps each statement
with its originating GALA line, which the Go compiler and runtime already honor.
No source-map file, no runtime cost. Imported GALA packages map too, so a single
trace can span library and application source with each frame pointing at real
<code>.gala</code> code. It's approximate inside constructs that lower to several Go
statements or to an IIFE (<code>match</code>, <code>if</code>-expressions, <code>bind</code>/<code>also</code>) — right
file, line possibly off by a few.</p>
<p><strong>Diagnostics got a frame.</strong> Errors now render Rust/Elm style, with a source
excerpt, a caret span, an inline label, and a hint:</p>
<pre><code>error[GALA-E0035]: bare Go builtin "len(...)" is not part of GALA's surface
  --&gt; bare_len.gala:4:13
  |
4 |     val n = len(s)
  |             ^^^ use `.Size()`
  |
  = hint: use `.Size()` (logical size — characters for strings) or `.ByteSize()` (raw bytes) instead of `len(...)`
</code></pre>
<p><strong>Guaranteed tail-call optimisation.</strong> A self-tail-recursive function with an
if-expression body compiles to a loop, so deep recursion runs in constant stack
space. This:</p>
<pre><code class="language-gala">func factorial(n int, acc int) int = if (n &lt;= 0) acc else factorial(n - 1, n * acc)
</code></pre>
<p>becomes exactly this Go:</p>
<pre><code class="language-go">func factorial(n int, acc int) int {
	for {
		if n &lt;= 0 {
			return acc
		} else {
			_tmp_1 := n - 1
			_tmp_2 := n * acc
			n, acc = _tmp_1, _tmp_2
			continue
		}
	}
}
</code></pre>
<p>Note the temporaries: both arguments are evaluated against the <em>old</em> parameter
values before either is reassigned, which is what makes <code>factorial(n-1, n*acc)</code>
mean what it says. The scope is deliberately narrow — plain functions (no
receiver), expression bodies, genuine self-tail-calls. Non-tail recursion like
<code>n * factorial(n-1)</code> is left alone rather than silently mis-transformed.</p>
<p><strong><code>use</code>, and closing the escape hatches.</strong> Scoped resource binding arrived, and
bare <code>defer</code>, bare <code>go</code>, and bare Go builtins were removed from GALA's surface —
routed through <code>use</code>, the <code>concurrent</code> package, and <code>go_interop</code> / <code>go_builtins</code>
respectively. Which is the same instinct as E0037: if the language offers a
checked path, the unchecked one shouldn't be the one that's easier to type.</p>
<p><strong>A <code>subprocess</code> package</strong>, with a goroutine-safe <code>Process</code> handle — async line
reads, async stdin writes, kill-after-timeout, all returning <code>Future</code>s.</p>
<hr />
<h2>Where this leaves things</h2>
<p>The pitch for GALA has been "the type system Go is missing." Sum types,
exhaustive matching, <code>Option</code> instead of <code>nil</code> — a compiler that rejects the
programs you didn't mean to write.</p>
<p>Concurrency safety is that same pitch aimed at the bug class Go developers
actually lose days to. Go's answer to data races is a runtime tool you remember
to run. GALA's answer is that the program doesn't build. Not because GALA is
cleverer, but because it spent its immutability budget early and can now cash it
in for something a mutable-by-default language can't buy at any price.</p>
<p>The check is young, and it guards declared boundaries rather than every
goroutine — reach around it and you are back to <code>go test -race</code>. Within those
boundaries it is still a different category of guarantee than sampling a
schedule and hoping.</p>
<p><code>gala.fyi</code> has the reference for
<a href="https://gala.fyi/features/concurrency-safety/">concurrency safety</a> and the full
<a href="https://gala.fyi/docs/errors/gala-e0037/">GALA-E0037</a> writeup, or you can try
the language without installing anything in the
<a href="https://gala-playground.fly.dev">playground</a>.</p>
]]></content:encoded></item><item><title><![CDATA[GALA in June: do-notation, applicative validation, and concurrent binds (0.56 -> 0.62)]]></title><description><![CDATA[A follow-up to The State of GALA: May 2026.
In May, GALA hit 0.50.0 with sealed types, exhaustive pattern matching, a real monad stack (Option, Either, Try, Future, IO), and a functional standard libr]]></description><link>https://martianov.dev/gala-june-2026-bind-also</link><guid isPermaLink="true">https://martianov.dev/gala-june-2026-bind-also</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[type systems]]></category><category><![CDATA[programming languages]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 05 Jul 2026 17:54:43 GMT</pubDate><content:encoded><![CDATA[<p><em>A follow-up to <a href="https://martianov.dev/state-of-gala-may-2026">The State of GALA: May 2026</a>.</em></p>
<p>In May, GALA hit 0.50.0 with sealed types, exhaustive pattern matching, a real monad stack (<code>Option</code>, <code>Either</code>, <code>Try</code>, <code>Future</code>, <code>IO</code>), and a functional standard library — all transpiling to plain Go. The one thing that stack was still missing was a decent way to <em>write</em> against it.</p>
<p>You had <code>Map</code> and <code>FlatMap</code>, which are great for a single linear pipeline and get ugly the moment a step needs a value from two steps back. Every intermediate value is trapped inside a closure, so the "graph" shape — where a later step reads an earlier one — pushes you into a staircase of nested <code>FlatMap</code> calls.</p>
<p>The headline of the June work (0.56 → 0.62) fixes exactly that. GALA now has <strong><code>bind</code> / <code>also</code></strong> — monadic do-notation, the piece that makes "Scala on Go" actually feel like writing Scala. Here's what shipped.</p>
<h2><code>bind</code>: the flat version of a <code>FlatMap</code> chain</h2>
<p>Take a small order pipeline. Fetch an order, validate it, charge it, build a receipt — where the receipt needs <em>both</em> the original order and the payment. With combinators, that last cross-reference forces nesting:</p>
<pre><code class="language-gala">fetchOrder(id).FlatMap((o) =&gt;
    validateOrder(o).FlatMap((valid) =&gt;
        chargePayment(valid).FlatMap((payment) =&gt;
            Success(Receipt(o.Id, payment)))))   // `o` survives only via nesting
</code></pre>
<p>With <code>bind</code>, every binding is a normal immutable local that stays in scope for the rest of the block:</p>
<pre><code class="language-gala">func processOrder(id int) Try[Receipt] {
    bind o       = fetchOrder(id)
    bind valid   = validateOrder(o)
    bind payment = chargePayment(valid)
    Success(Receipt(o.Id, payment))    // `o` still in scope — no nesting
}
</code></pre>
<p><code>bind name = expr</code> unwraps the monad, binds the success value, and short-circuits the <em>whole block</em> on the first <code>Failure</code> — so <code>processOrder(0)</code> stops at the first <code>bind</code> and returns that <code>Failure</code> unchanged. This isn't Rust's <code>?</code> or Zig's <code>try</code>: it's not a hidden return that hijacks the enclosing function. The block is an ordinary expression of type <code>Try[Receipt]</code> — you can name it, return it, or pass it around. It just desugars, mechanically, to the nested <code>FlatMap</code> chain above.</p>
<p>If you've written a Scala for-comprehension or a Haskell <code>do</code> block, this is the same idea with a keyword that tells you it can short-circuit, instead of a <code>&lt;-</code> you have to learn to read.</p>
<h2><code>also</code>: independent binds, and why the keyword earns its keep</h2>
<p>Not every step depends on the one before it. Validating a name, an email, and an age are three independent checks. Sequencing them with <code>bind</code> is a lie about the data flow — and it throws away everything except the <em>first</em> error.</p>
<p><code>also</code> marks a bind as independent of its group:</p>
<pre><code class="language-gala">func sum2(x string, y string) Option[int] {
    bind a = lookup(x)
    also b = lookup(y)      // independent of `a`
    Some(a + b)
}
</code></pre>
<p>A leading <code>bind</code> plus one or more <code>also</code> clauses form a <em>product group</em>. The clauses may not reference each other — and that non-dependence is exactly what licenses the compiler to do something smarter than sequencing. What "smarter" means is <strong>decided by the type</strong>:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>What an <code>also</code> group does</th>
</tr>
</thead>
<tbody><tr>
<td><code>Try</code> / <code>Option</code> / <code>Either</code></td>
<td>sequential short-circuit on the first failure</td>
</tr>
<tr>
<td><code>Validated</code></td>
<td><strong>accumulates every error</strong></td>
</tr>
<tr>
<td><code>Future</code></td>
<td>runs the clauses <strong>concurrently</strong></td>
</tr>
</tbody></table>
<p>One keyword, three behaviors, no annotations — the block's type picks the semantics.</p>
<h2><code>Validated</code>: collect all the errors, not just the first</h2>
<p>Fail-fast is wrong for form validation. If a user submits a blank name, a blank email, and a negative age, you want to tell them all three things at once, not make them fix them one round-trip at a time. That's what the new <code>Validated[E, A]</code> type is for — it's a distinct sealed type (<code>Valid</code> / <code>Invalid</code>), kept deliberately separate from <code>Either</code>'s fail-fast semantics.</p>
<pre><code class="language-gala">import . "martianoff/gala/validation"

func vName(s string)  Validated[string, string] = if (s != "") Valid(s) else InvalidOf("name required")
func vEmail(s string) Validated[string, string] = if (s != "") Valid(s) else InvalidOf("email required")
func vAge(n int)      Validated[string, int]    = if (n &gt;= 0)  Valid(n) else InvalidOf("age negative")

func makePerson(name string, email string, age int) Validated[string, Person] {
    bind n = vName(name)
    also e = vEmail(email)
    also a = vAge(age)
    Valid(Person(n, e, a))
}
</code></pre>
<p>Because the clauses are <code>also</code> (independent), the group lowers to <code>Validated.Zip3(...).FlatMap(...)</code>, and <code>Validated</code>'s <code>Zip</code> pre-collects every clause's errors before combining. So <code>makePerson("", "", -1)</code> doesn't stop at the blank name — it reports all three:</p>
<pre><code class="language-gala">val bad = makePerson("", "", -1)
Println(s"errors: ${bad.GetErrors().Size()}")   // 3
</code></pre>
<p>Note there isn't a single explicit type argument in that code. <code>Valid</code> / <code>Invalid</code> fix their phantom type parameter from the declared return type, and <code>InvalidOf</code> infers its instantiation from context — implicit typing all the way down, the way GALA code is supposed to read.</p>
<h2><code>also</code> over <code>Future</code>: structured concurrency for free</h2>
<p>The same <code>also</code> over <code>Future</code> runs the clauses <em>concurrently</em>:</p>
<pre><code class="language-gala">import . "martianoff/gala/concurrent"

func total() Future[int] {
    bind a = compute(2)
    also b = compute(3)    // these three
    also c = compute(4)    // run in parallel
    Future[int](a + b + c)
}
</code></pre>
<p>The group lowers to <code>Future.Zip3(...)</code>, which starts every future and joins them — so the block waits on all three in parallel instead of threading each through the next. Concurrency is <em>visible in the source</em>: sequential <code>bind</code>s stay sequential, only an <code>also</code> group runs in parallel. The transpiler never auto-parallelizes consecutive <code>bind</code>s behind your back.</p>
<p>(While we were in <code>Future</code>, the async constructor got simpler too: <code>Future(...)</code> is now the way to start an async computation, and the old <code>FutureApply</code> spelling is gone.)</p>
<h2>The part I'm proud of: it works over <em>your</em> monads</h2>
<p>Here's the design constraint that made this interesting. GALA's standing rule is that the standard library gets <strong>no special treatment</strong> — <code>Try</code>, <code>Option</code>, <code>Either</code>, <code>Future</code> have to resolve through the same mechanism a third-party type would. And Go's generics can't express a higher-kinded <code>Bindable[F[_]]</code> interface, so GALA doesn't fake one.</p>
<p>Instead, <code>bind</code>/<code>also</code> are resolved <strong>structurally, at transpile time, on the block's concrete type</strong> — before Go ever sees the code. A type becomes bindable by providing exactly one method:</p>
<pre><code class="language-gala">func (m M[T]) FlatMap[U any](f func(T) M[U]) M[U]
</code></pre>
<p>That's the whole contract. Here's a user-defined <code>Step</code> monad with no relationship to the standard library, and <code>bind</code> works over it identically to <code>Try</code>:</p>
<pre><code class="language-gala">sealed type Step[T any] {
    case Go(Value T)
    case Stop(Reason string)
}

func (s Step[T]) FlatMap[U any](f func(T) Step[U]) Step[U] = s match {
    case Go(v)   =&gt; f(v)
    case Stop(r) =&gt; Stop[U](r)
}

func pipeline(n int) Step[int] {
    bind a = start(n)
    bind b = twice(a)
    Go(a + b)          // `a` still in scope; a `Stop` anywhere short-circuits
}
</code></pre>
<p>The short-circuit behavior lives entirely inside the author's <code>FlatMap</code> — the desugaring never inspects, and never hardcodes, which variant is the "zero." Supply a <code>Pure</code>/constructor and you also get auto-lift of trailing plain values and sequential <code>also</code>; supply a custom <code>Zip</code> and you get concurrency or error accumulation. A brand-new user monad gets <code>bind</code> and sequential <code>also</code> for free. This is the OCaml <code>let*</code> / <code>and*</code> model, resolved structurally instead of lexically. There is no built-in list of "blessed" monads.</p>
<h2>The rest of June</h2>
<p>Beyond <code>bind</code>/<code>also</code>, 0.56 → 0.62 was mostly the unglamorous work that a compiler needs to actually be trusted:</p>
<ul>
<li><strong><code>Zip</code> up to <code>Zip10</code></strong> on the product types, so <code>also</code> groups can be wide.</li>
<li><strong>By-name thunk sugar</strong> for zero-argument function parameters — pass an expression where a <code>func()</code> is expected and it's wrapped for you.</li>
<li><strong>Race-free concurrent parsing.</strong> Multi-file packages now parse in parallel with a per-parse prediction cache, so the parser is safe under concurrency.</li>
<li><strong>A stack of type-inference fixes</strong>: inferring a generic method's result type from a lambda's return under type-parameter name collisions; inferring "phantom" return-only type parameters from the expected type; inferring lambda parameter types in typed contexts (and <em>rejecting</em> truly-untyped ones instead of silently widening to <code>any</code>); statement-position <code>match</code> dispatch with a guarded wildcard before the default.</li>
<li><strong>A standard-library cleanup pass</strong> applying GALA's own best practices across <code>std</code>.</li>
</ul>
<p>Every one of those inference fixes shipped with a permanent example test, per the project rule that a transpiler bug becomes a repro before it becomes a fix.</p>
<h2>What's still rough</h2>
<p>Same honest list as always:</p>
<ul>
<li><strong>Package registry.</strong> Dependencies resolve — Go and GALA, including third-party Go modules — but there's still no first-class public registry for publishing and discovering GALA libraries. It remains the top open roadmap item.</li>
<li><strong><code>bind</code> ergonomics at the edges.</strong> Heterogeneous binds (binding an <code>Option</code> inside a <code>Try</code> block, say) need a resolvable <code>Lift</code>, and lifting a "zero" type into an error type makes you supply the error explicitly. That's by design — no silent coercions — but it's a sharp edge you'll meet.</li>
<li><strong>Maturity.</strong> GALA is six months old. It generates readable Go and runs real software — a TUI framework, a multi-agent orchestrator, this project's own tooling — but it hasn't been load-bearing in a large team's production system for a year. Use it with eyes open.</li>
</ul>
<h2>Try it</h2>
<p><code>bind</code>/<code>also</code> is the feature I most wanted GALA to have, because it's the one that turns a pile of good combinators into something that reads like the functional languages it's borrowing from. If you've missed for-comprehensions or <code>do</code>-notation in Go, this is the part to try first.</p>
<p><strong><a href="https://gala-playground.fly.dev">Try it in your browser</a></strong> — no install — or grab a binary from <a href="https://github.com/martianoff/gala/releases">Releases</a> and <code>gala run main.gala</code>.</p>
]]></content:encoded></item><item><title><![CDATA[From Zero to a Go Module on Bazel in One Sitting: Onboarding to GALA 0.53]]></title><description><![CDATA[There's a specific moment that decides whether a new language is worth your time: the first time you try to use something from the ecosystem you already depend on. For a language that transpiles to Go]]></description><link>https://martianov.dev/from-zero-to-a-go-module-on-bazel-onboarding-gala-053</link><guid isPermaLink="true">https://martianov.dev/from-zero-to-a-go-module-on-bazel-onboarding-gala-053</guid><category><![CDATA[programming languages]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[compiler]]></category><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Sun, 14 Jun 2026 00:53:35 GMT</pubDate><content:encoded><![CDATA[<p>There's a specific moment that decides whether a new language is worth your time: the first time you try to use something from the ecosystem you already depend on. For a language that transpiles to Go, that means pulling in a third-party Go module and having it <em>just work</em> — concrete types, real build, no <code>interface{}</code> leaking through.</p>
<p>I wanted to know if GALA passes that test today, so I did the honest thing: started in an empty directory with nothing but the <code>gala</code> CLI, Go, and Bazel on my PATH, and tried to build a program that imports a Go module through Bazel and <code>rules_gala</code>. No copying an example repo, no insider shortcuts. This post is the actual transcript, friction and all.</p>
<p>The short version: it works, the types come out concrete, and the whole thing is about four small files you write by hand plus two generated ones. There's exactly one wiring detail the main docs get wrong, and I'll show you the version that actually works.</p>
<hr />
<h2>The goal</h2>
<p>A program that imports <a href="https://github.com/google/uuid"><code>github.com/google/uuid</code></a> — a real third-party Go module, not stdlib — generates a UUID, and prints it. Built and run entirely through Bazel with <code>rules_gala</code>. The interesting part isn't the UUID; it's whether GALA can read a third-party Go package's types well enough to produce concrete, compiling Go.</p>
<p>Versions used: <code>gala</code> 0.53.1, Go 1.25.5, Bazel 8.</p>
<hr />
<h2>Step 1 — Initialize the module and add the Go dependency</h2>
<pre><code class="language-bash">gala mod init example.com/hellouuid
gala mod add github.com/google/uuid@v1.6.0 --go
</code></pre>
<pre><code>Initialized gala.mod for module example.com/hellouuid
Fetching github.com/google/uuid@v1.6.0...
Added github.com/google/uuid@v1.6.0 (Go dependency)
</code></pre>
<p>The <code>--go</code> flag is the whole story of GALA's dependency model: it marks a requirement as a <em>Go</em> package — fetched and linked, never transpiled — as opposed to a GALA library, which is fetched as <code>.gala</code> source and transpiled at build time. The resulting <code>gala.mod</code> is deliberately Go-mod-shaped:</p>
<pre><code>module example.com/hellouuid

gala 0.53.1

require github.com/google/uuid v1.6.0 // go
</code></pre>
<p>No <code>go.mod</code> in your project yet, and that's intentional — GALA projects are driven by <code>gala.mod</code>.</p>
<h2>Step 2 — Write the program</h2>
<pre><code class="language-gala">package main

import "github.com/google/uuid"

func main() {
    val id = uuid.New()
    Println(s"Fresh UUID: ${id.String()}")
}
</code></pre>
<p>That's the entire program. <code>uuid.New()</code> is a Go function returning a <code>uuid.UUID</code>; <code>id.String()</code> is a method on that Go type. <code>Println</code> and the <code>s"..."</code> interpolation are GALA built-ins — no <code>fmt</code> import needed. The question is whether the transpiler knows what <code>uuid.New()</code> returns. Hold that thought.</p>
<h2>Step 3 — The Bazel wiring</h2>
<p>Four files. The <code>.bazelrc</code> selects the <code>rules_gala</code> registry and passes the Go SDK through to the transpiler:</p>
<pre><code>common --enable_bzlmod
common --noenable_workspace
build  --enable_runfiles

common --registry=https://raw.githubusercontent.com/martianoff/rules-gala/main/
common --registry=https://bcr.bazel.build

build  --action_env=GOROOT
build  --action_env=PATH
</code></pre>
<p><code>MODULE.bazel</code> is normal bzlmod — the GALA toolchain, Go rules, and the Gazelle extension:</p>
<pre><code class="language-starlark">module(name = "hellouuid", version = "0.0.1")

bazel_dep(name = "rules_gala", version = "0.1.3")
bazel_dep(name = "gala", version = "0.53.1")
register_toolchains("@gala//tools/toolchain:gala_toolchain")

bazel_dep(name = "rules_go", version = "0.50.1")
bazel_dep(name = "gazelle", version = "0.39.1")
bazel_dep(name = "gala_gazelle", version = "0.2.5", dev_dependency = True)

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(go_deps, "com_github_google_uuid")
</code></pre>
<p>And the root <code>BUILD.bazel</code> — <strong>this is the one line that matters</strong>:</p>
<pre><code class="language-starlark">load("@gala_gazelle//gala:defs.bzl", "gala_gazelle")

gala_gazelle(name = "gazelle")
</code></pre>
<blockquote>
<p><strong>The one gotcha.</strong> If you reach for the plain <code>gazelle(name = "gazelle")</code> rule
from <code>@gazelle//:def.bzl</code> — the one you'd use in a pure-Go project — Gazelle
only knows the Go language. It greets you with
<code>unknown directive: gazelle:gala_prefix</code> and generates <strong>zero</strong> GALA targets.
The fix is the <code>gala_gazelle</code> macro above: it builds a <em>composite</em> Gazelle
binary that embeds both the Go language and the GALA language, plus a
toolchain-driven import helper. Use it and BUILD generation covers Go, GALA,
and mixed packages in one pass.</p>
</blockquote>
<h2>Step 4 — Generate, build, run</h2>
<p><code>gala mod tidy</code> notices the project is now a Bazel project (because <code>MODULE.bazel</code> exists) and emits a portable <code>go.mod</code>/<code>go.sum</code> carrying just the Go dependencies:</p>
<pre><code class="language-bash">gala mod tidy
</code></pre>
<p>Then Gazelle writes the build target for you:</p>
<pre><code class="language-bash">bazel run //:gazelle
</code></pre>
<p>It appends a <code>gala_binary</code> to <code>BUILD.bazel</code>, with the Go dependency already resolved to its Bazel label — I didn't type this:</p>
<pre><code class="language-starlark">gala_binary(
    name = "hellouuid",
    srcs = ["main.gala"],
    deps = ["@com_github_google_uuid//:uuid"],
)
</code></pre>
<p>Build and run:</p>
<pre><code class="language-bash">bazel build //...
bazel run //:hellouuid
</code></pre>
<p>A real UUID, from a real Go module, through the full Bazel pipeline.</p>
<hr />
<h2>The part that actually matters: the types are concrete</h2>
<p>Anyone can make <code>interface{}</code> compile. The test of "Go interop" is whether the transpiler <em>understood</em> the third-party type. Here's the Go that GALA generated for <code>main.gala</code>:</p>
<pre><code class="language-go">// Code generated by GALA transpiler. DO NOT EDIT.
package main

import "fmt"
import "martianoff/gala/std"
import "github.com/google/uuid"

func main() {
    var id = std.NewImmutable(uuid.New())
    fmt.Println(fmt.Sprintf("Fresh UUID: %v", id.Get().String()))
}
</code></pre>
<p><code>uuid.New()</code> is called directly and its concrete <code>uuid.UUID</code> result flows into <code>id.Get().String()</code>. There is no <code>any</code>, no type assertion, no reflection. If the transpiler had failed to resolve <code>uuid.New()</code>'s return type, <code>id</code> would have been <code>any</code> and <code>.String()</code> would not compile — the build would have failed, not silently degraded. It compiled, so the type resolution is real.</p>
<p>This is the capability that landed recently: GALA reads the <strong>Go SDK at transpile time</strong> to resolve function signatures, struct fields, and method sets from third-party modules — not just stdlib. That's why the <code>.bazelrc</code> passes <code>GOROOT</code> and <code>PATH</code> into the build actions. It's also why <code>(T, error)</code> returns from Go functions get auto-wrapped into <code>Try[T]</code> at the call site: the transpiler can see the shape of what it's calling.</p>
<hr />
<h2>Was it genuinely simple? An honest scorecard</h2>
<p>Yes — with one caveat I won't paper over.</p>
<p><strong>What you write by hand:</strong> <code>main.gala</code> (6 lines), <code>.bazelrc</code> (~8 lines), <code>MODULE.bazel</code> (~18 lines), <code>BUILD.bazel</code> (3 lines + a directive). <strong>What's generated for you:</strong> <code>go.mod</code>/<code>go.sum</code> (by <code>gala mod tidy</code>) and the <code>gala_binary</code> target (by Gazelle). The dependency label resolution — the part that's tedious and error-prone in hand-written Bazel — is automatic.</p>
<p><strong>The friction, named honestly:</strong></p>
<ul>
<li><strong>The Gazelle wiring.</strong> The working recipe is the <code>gala_gazelle</code> macro from <code>@gala_gazelle//gala:defs.bzl</code>, documented in <a href="https://github.com/martianoff/rules-gala">rules-gala's <code>gazelle/README.md</code></a>. If you follow only the main repo's dependency-management doc, which shows the plain Go <code>gazelle</code> rule, you'll hit <code>unknown directive: gazelle:gala_prefix</code> and produce no GALA targets. This is the single thing most likely to trip up a newcomer. (It's a doc gap, not a tooling gap — once you use the macro, it just works.)</li>
<li><strong>Order of operations.</strong> <code>gala mod tidy</code> only generates <code>go.mod</code> once <code>MODULE.bazel</code> is present — it detects "Bazel project" by that file. Run the Bazel scaffolding first, then <code>tidy</code>.</li>
<li><strong>Cold build cost.</strong> The first <code>bazel run //:gazelle</code> builds the GALA toolchain and the Go SDK from the registry — about a minute and a half on a cold cache here. Every build after that is single-digit seconds. This is ordinary Bazel behavior, not a GALA quirk.</li>
<li><strong>Bazel-first.</strong> This is the Bazel path. For a quick single binary you can skip all of it and use <code>gala build</code> / <code>gala run</code> directly; the Bazel story is for projects that want reproducible, registry-pinned, polyglot builds.</li>
</ul>
<p>None of these are blockers. The worst case costs you one web search to find the right Gazelle macro. After that, adding another Go dependency is two commands — <code>gala mod add … --go</code> then <code>bazel run //:gazelle</code> — and the BUILD file updates itself.</p>
<hr />
<h2>Why this is a bigger deal than a UUID</h2>
<p>The reason "import a Go module" is the make-or-break test is that it's the entire value proposition of a transpile-to-Go language. If GALA's interop were shallow — stdlib only, or types collapsing to <code>any</code> at the boundary — then every time you reached for the Go ecosystem you'd fall out of the type system that made you choose GALA in the first place. You'd be writing Go with extra steps.</p>
<p>That's not what happened here. A third-party module dropped in, kept its types, and built through a production-grade Bazel toolchain with generated BUILD files. The recent work — third-party Go type inference, the extraction of <code>rules_gala</code> into its own registry-published module, and first-class Gazelle support — adds up to a build story where the polyglot seam is close to invisible. You get sealed types, exhaustive pattern matching, and <code>Option</code>/<code>Try</code>/<code>Either</code> on the GALA side, and the entire Go module ecosystem on the other, with concrete types across the boundary.</p>
<p>Try it before you commit to anything: the <a href="https://gala-playground.fly.dev">GALA Playground</a> runs in the browser with no install. When you're ready for a real project, the steps above are the whole recipe — and <code>rules_gala</code> plus Gazelle make sure the build is the part you stop thinking about.</p>
]]></content:encoded></item><item><title><![CDATA[Productionizing the GALA Build Stack: rules_gala, a Real Toolchain, and Gazelle]]></title><description><![CDATA[A language that transpiles to Go lives or dies by its build story. The language features can be elegant — sealed types, exhaustive pattern matching, type inference — but if wiring GALA into a real pro]]></description><link>https://martianov.dev/productionizing-the-gala-build-stack-rules-gala-toolchain-gazelle</link><guid isPermaLink="true">https://martianov.dev/productionizing-the-gala-build-stack-rules-gala-toolchain-gazelle</guid><category><![CDATA[Go Language]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[compiler]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Fri, 05 Jun 2026 06:39:08 GMT</pubDate><content:encoded><![CDATA[<p>A language that transpiles to Go lives or dies by its build story. The language features can be elegant — sealed types, exhaustive pattern matching, type inference — but if wiring GALA into a real project means hand-writing brittle build rules and babysitting a transpilation step, nobody ships it. For most of GALA's life the build rules lived <em>inside</em> the language repo, bolted directly to its internals. That was fine for bootstrapping the compiler. It was not fine for anyone trying to build a real GALA project alongside their Go code.</p>
<p>The last few releases have been about fixing that. GALA 0.51.0 extracted the Bazel rules into a standalone, registry-published module — <a href="https://github.com/martianoff/rules-gala"><code>rules_gala</code></a> — sitting behind a proper Bazel toolchain. GALA 0.52.0 added first-class Gazelle support, so the <code>BUILD</code> files that wire everything together are now generated instead of hand-authored. This post is about that direction: turning GALA's build tooling from "works on the maintainer's machine" into something production-grade and reusable across modules.</p>
<hr />
<h2>The problem: rules welded to the language</h2>
<p>When the build rules ship inside the language repo, every consumer loads them straight from it:</p>
<pre><code class="language-starlark">load("@gala//:gala.bzl", "gala_library", "gala_binary", "gala_go_test")
</code></pre>
<p>This looks harmless, and it works. But it quietly couples three things that should be independent:</p>
<ol>
<li><strong>Your project's build logic</strong> depends on the <em>internal layout</em> of the language repo. The <code>@gala//:gala.bzl</code> path is an implementation detail, not a stable API. If the compiler repo reorganizes, your <code>BUILD</code> files break.</li>
<li><strong>The rules hardcode where the tools live.</strong> The transpiler binary, the stdlib filegroup, the test runner — every rule reached for them with a fixed <code>Label("//...")</code> pointing back into the language repo. There was exactly one way to assemble a GALA build, and it was the language repo's way.</li>
<li><strong>Versioning is muddled.</strong> "Which version of the rules am I on?" and "which version of the compiler am I on?" were the same question. You couldn't upgrade your build logic without upgrading the transpiler, or vice versa.</li>
</ol>
<p>None of this matters when the only consumer of the rules is the language repo itself. It matters a lot the moment you have several GALA projects — a server library, a TUI framework, an application — that each want to pin versions and evolve independently. The rules needed to become a real, separately versioned dependency.</p>
<hr />
<h2>rules_gala: a registry-published Bazel module</h2>
<p>GALA 0.51.0 split the build stack cleanly in two.</p>
<p>The <strong>language repo</strong> keeps what only it can provide: the transpiler binaries, the standard library, the test framework, and a small set of <em>toolchain bindings</em>.</p>
<p>Everything else — the rule definitions, the bzlmod extension, the repository rules, the library/binary/test macros — moved into <a href="https://github.com/martianoff/rules-gala"><code>rules_gala</code></a>, which is published through its own Bazel module registry.</p>
<p>Consuming it is two lines in <code>.bazelrc</code> to select the registry (order matters — the custom registry must come first so it wins for <code>rules_gala</code>):</p>
<pre><code>common --registry=https://raw.githubusercontent.com/martianoff/rules-gala/main/
common --registry=https://bcr.bazel.build
</code></pre>
<p>and a normal <code>bazel_dep</code> in <code>MODULE.bazel</code>:</p>
<pre><code class="language-starlark">bazel_dep(name = "rules_gala", version = "0.1.1")
bazel_dep(name = "gala", version = "0.51.0")

register_toolchains(
    "@gala//tools/toolchain:gala_toolchain",
    "@gala//tools/toolchain:gala_bootstrap_toolchain",
)
</code></pre>
<p>The load path now points at the rules module, not the language internals:</p>
<pre><code class="language-starlark">load("@rules_gala//gala:defs.bzl", "gala_library", "gala_binary", "gala_test")

gala_library(
    name = "mylib",
    srcs = ["mylib.gala"],
    importpath = "example.com/mylib",
    visibility = ["//visibility:public"],
)

gala_test(
    name = "mylib_test",
    srcs = ["mylib_test.gala"],
    deps = [":mylib"],
)
</code></pre>
<p>This is a breaking change — load paths and macro names both moved — but it's the kind of break that buys you a stable surface afterward. The rule set that <code>rules_gala</code> exposes is small and deliberately boring:</p>
<table>
<thead>
<tr>
<th>Rule</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>gala_library</code></td>
<td>Build a GALA library (<code>go_library</code> under the hood).</td>
</tr>
<tr>
<td><code>gala_binary</code></td>
<td>Build a GALA executable.</td>
</tr>
<tr>
<td><code>gala_test</code></td>
<td>Auto-discovers and runs every <code>func TestXxx(t T) T</code>. The canonical test rule.</td>
</tr>
<tr>
<td><code>gala_exec_test</code></td>
<td>Diffs program stdout against a checked-in expected file.</td>
</tr>
<tr>
<td><code>gala_unit_test</code></td>
<td>Wraps a <code>gala_binary</code> as a test (exit-code-only).</td>
</tr>
<tr>
<td><code>gala_transpile</code></td>
<td>Transpile a single <code>.gala</code> file to <code>.go</code>.</td>
</tr>
<tr>
<td><code>gala_transpile_package</code></td>
<td>Batch-transpile all <code>.gala</code> files in a package in one process.</td>
</tr>
<tr>
<td><code>gala_bootstrap_transpile</code></td>
<td>Transpile with the bootstrap binary (stdlib use only).</td>
</tr>
</tbody></table>
<p>While extracting the rules, the test surface also got a naming cleanup that had been overdue. The canonical "discover and run <code>TestXxx</code> functions" rule is now simply <code>gala_test</code>; the stdout-diffing rule became <code>gala_exec_test</code>:</p>
<pre><code class="language-diff">- gala_go_test   # discovers TestXxx functions
+ gala_test

- gala_test      # diffs stdout against an expected file
+ gala_exec_test
</code></pre>
<p>There are no compatibility aliases. That's a deliberate beta-era choice: rename every call site once, at upgrade time, rather than carry two names for the same thing forever.</p>
<hr />
<h2>The piece that makes it reusable: a toolchain</h2>
<p>Moving files around doesn't make rules reusable on its own. The real change in 0.51.0 is <em>how the rules find their tools</em>.</p>
<p>Instead of hardcoding <code>Label("//cmd/gala:gala")</code> and friends, every rule in <code>@rules_gala//gala:defs.bzl</code> resolves its tools through a Bazel <strong>toolchain</strong> — <code>@rules_gala//gala:toolchain_type</code>. The language repo registers an implementation of that toolchain backed by its in-repo binaries, stdlib, and test framework. But because the rules only know about the <em>toolchain type</em>, anyone can supply their own implementation:</p>
<pre><code class="language-starlark">load("@rules_gala//gala:toolchain.bzl", "gala_toolchain")

gala_toolchain(
    name = "my_gala_toolchain_impl",
    gala_binary      = "//path/to:gala",
    gala_worker      = "//path/to:gala_worker",
    gala_bootstrap   = "//path/to:gala_bootstrap",
    test_runner      = "//path/to:gala_test_runner",
    test_gen         = "//path/to:gala_test_gen",
    all_gala_sources = "//:all_gala_sources",
    go_mod           = "//:go.mod",
)

toolchain(
    name = "my_gala_toolchain",
    toolchain = ":my_gala_toolchain_impl",
    toolchain_type = "@rules_gala//gala:toolchain_type",
)
</code></pre>
<p>This is the standard Bazel toolchain pattern — the same idea behind <code>rules_go</code>'s Go SDK toolchain. The rules describe <em>what</em> to do with a GALA source file; the toolchain provides <em>which</em> binaries do it. You could build against a locally-built transpiler, a pinned release, or a patched fork without touching a single <code>BUILD</code> file.</p>
<p>There's one subtlety worth calling out, because it's the kind of thing that bites you in a transpiled language. The stdlib itself is written in GALA and has to be transpiled — but the main toolchain's worker <em>embeds</em> the stdlib. If stdlib targets depended on the main toolchain, you'd have a cycle: stdlib → toolchain → worker → embeds stdlib. So <code>gala_bootstrap_transpile</code> resolves through a <em>separate</em> <code>bootstrap_toolchain_type</code> carrying only the bootstrap binary, which has no stdlib dependency. The split is what lets the language build its own standard library hermetically. It's a small detail, but it's exactly the kind of thing that has to be right for the build to be reproducible from a cold cache.</p>
<hr />
<h2>Gazelle: stop hand-writing BUILD files</h2>
<p>A clean rule set still leaves you writing <code>BUILD.bazel</code> by hand — listing sources, and worse, manually tracking the <code>deps</code> between GALA packages as imports change. For a Go project you'd never accept that; Gazelle generates and maintains those files for you. GALA 0.52.0 brought the same thing to GALA.</p>
<p>The mechanism is a small one, and it leans on something the compiler now exposes directly. GALA 0.52.0 added a parse-only subcommand:</p>
<pre><code>gala imports --json path/to/file.gala
</code></pre>
<p>This walks the ANTLR parse tree and emits the package and its imports as JSON — <em>no transpilation, no Go SDK required</em>. It's deliberately cheap, because it runs once per file during <code>BUILD</code> generation. That subcommand is the helper the GALA Gazelle extension shells out to in order to resolve dependencies.</p>
<p>The extension ships from <code>rules_gala</code> as the <code>@gala_gazelle</code> Bazel module and is bundled together with Gazelle's Go language into a single <code>gazelle_binary</code>. The payoff is that one command maintains <em>everything</em>:</p>
<pre><code>bazel run //:gazelle
</code></pre>
<p>That single pass generates and updates <code>BUILD</code> files for Go packages, GALA packages, and mixed GALA+Go packages together. You steer it with <code># gazelle:</code> directives — most importantly <code>gala_prefix</code>, which sets your module's import prefix so generated <code>deps</code> resolve correctly:</p>
<pre><code class="language-starlark">
</code></pre>
<p>Two things are worth knowing before you run it:</p>
<ul>
<li><strong>The helper needs a <code>gala</code> on <code>PATH</code> new enough to support <code>gala imports --json</code>.</strong> An older CLI fails with <code>unknown flag: --json</code> and silently resolves no GALA deps. If your generated <code>deps</code> come back empty, check the CLI version first.</li>
<li><strong>Mixed hand-written <code>.go</code> + <code>.gala</code> packages are left alone on purpose.</strong> When Gazelle detects a package that mixes the two, it backs off rather than guessing at a <code>gala_library</code> — those stay on manual <code>gala_bootstrap_transpile</code> + <code>go_library</code> wiring. Generating the easy 90% and refusing to corrupt the hard 10% is the right default for a tool you run on every commit.</li>
</ul>
<p>If you also want module dependencies resolved from a manifest, the bzlmod extension reads them from <code>gala.mod</code>:</p>
<pre><code class="language-starlark">gala = use_extension("@rules_gala//gala:extensions.bzl", "gala")
gala.from_file(gala_mod = "//:gala.mod")
</code></pre>
<hr />
<h2>What "production-grade" actually means here</h2>
<p>Putting the pieces together, a GALA project's build now has the same properties you'd expect from a mature Go-plus-Bazel setup:</p>
<ul>
<li><strong>Reproducible.</strong> Tools are resolved through a toolchain and pinned through the module registry. A cold-cache build pulls a known <code>rules_gala</code> and a known <code>gala</code>, not whatever happens to be checked out.</li>
<li><strong>Reusable across modules.</strong> Because the rules talk to a toolchain <em>type</em> rather than hardcoded labels, a server library, a TUI framework, and an application can each depend on <code>rules_gala</code> and pin their own compiler version.</li>
<li><strong>Independently versioned.</strong> Build logic (<code>rules_gala</code>) and the compiler (<code>gala</code>) now move on separate tracks. You can upgrade one without the other.</li>
<li><strong>One command for BUILD files.</strong> <code>bazel run //:gazelle</code> covers Go, GALA, and mixed packages — no hand-maintained dependency lists.</li>
</ul>
<p>None of this is glamorous. That's the point. The best build tooling is the kind you stop thinking about, and the direction here is to make GALA's build invisible the way <code>go build</code> is invisible.</p>
<hr />
<h2>Honest limitations</h2>
<p>This is beta tooling, and a few rough edges are worth naming:</p>
<ul>
<li><strong>Mixed GALA+Go packages are still manual.</strong> Gazelle deliberately won't touch them, so you wire those by hand. If your project is heavily intermixed, expect to write some <code>BUILD</code> rules yourself.</li>
<li><strong>The Gazelle helper depends on a recent CLI.</strong> The version coupling between the <code>gala</code> on your <code>PATH</code> and the <code>--json</code> flag is a real footgun until the ecosystem settles.</li>
<li><strong>Bazel-first.</strong> This whole story assumes Bazel. There's a <code>gala run</code>/<code>gala build</code> CLI path for small programs, but the production build experience described here is Bazel-centric. If your shop isn't on Bazel, this is not yet for you.</li>
<li><strong>The API just broke.</strong> 0.51.0 moved every load path and renamed test rules with no aliases. That's the right call for a beta, but it means upgrading is a real, if mechanical, migration.</li>
</ul>
<hr />
<h2>Try it</h2>
<p>If you're starting a new GALA project on Bazel, the setup is the <code>.bazelrc</code> registry lines and the <code>MODULE.bazel</code> <code>bazel_dep</code>s above, then <code>bazel run //:gazelle</code> to generate your <code>BUILD</code> files. The rule reference and directive list live in the <a href="https://github.com/martianoff/rules-gala"><code>rules_gala</code> repository</a>, and the migration checklist for existing projects is in the <a href="https://github.com/martianoff/gala/releases">GALA 0.51.0 release notes</a>.</p>
<p>To see the language itself before committing to a build setup, the <a href="https://gala-playground.fly.dev">GALA Playground</a> runs everything in the browser with no installation. And if you just want to read GALA code, the <a href="https://github.com/martianoff/gala">main repository</a> has the full language spec and a growing set of examples.</p>
<p>The language work — sealed types, pattern matching, type inference — is what makes GALA worth writing. The build work is what makes it worth <em>shipping</em>. Getting <code>rules_gala</code> and Gazelle right is how GALA stops being a clever transpiler and starts being something you can put in production.</p>
]]></content:encoded></item><item><title><![CDATA[The State of GALA: May 2026]]></title><description><![CDATA[A follow-up to The State of GALA: March 2026.
In March I wrote up where GALA stood: version 0.25.2, a couple of months old, 46 releases deep, with a rich standard library, a zero-reflection JSON codec]]></description><link>https://martianov.dev/state-of-gala-may-2026</link><guid isPermaLink="true">https://martianov.dev/state-of-gala-may-2026</guid><category><![CDATA[programming languages]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[compiler]]></category><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Maksim Martianov]]></dc:creator><pubDate>Fri, 29 May 2026 14:01:36 GMT</pubDate><content:encoded><![CDATA[<p><em>A follow-up to <a href="https://martianov.dev/state-of-gala-march-2026">The State of GALA: March 2026</a>.</em></p>
<p>In March I wrote up where GALA stood: version 0.25.2, a couple of months old, 46 releases deep, with a rich standard library, a zero-reflection JSON codec, and async primitives. I also said GALA was "still young" and named its roughest edge by name — complex generic type inference across package boundaries — and listed four near-term priorities: stability, cross-package generics, IDE support, and a package registry.</p>
<p>Eleven weeks later GALA is at <strong>0.50.0</strong>. Another ~55 releases shipped in that window, which works out to roughly one a working day — the cadence from the first two months held. More interesting than the version number is what changed underneath it. Three of the four roadmap items landed (one of them more thoroughly than I'd planned), the standard library grew four new packages, the build system got a real caching layer, and — the part I keep coming back to — the one-line pitch changed.</p>
<h2>We stopped calling it "the Go Alternative Language"</h2>
<p>In March, the README led with the backronym: GALA, the <strong>Go Alternative LAnguage</strong>. Accurate, but it told you what GALA <em>isn't</em> (plain Go) rather than what it <em>is</em>. The tagline now reads:</p>
<blockquote>
<p><strong>Scala on Go.</strong></p>
</blockquote>
<p>That's a sharper, more honest claim, and it's the one people actually reacted to. GALA is a statically typed, functional-first language with sealed types, exhaustive pattern matching, immutable collections, and a real monad stack — <code>Option</code>, <code>Either</code>, <code>Try</code>, <code>Future</code>, <code>IO</code> — that happens to compile to readable Go and keep the entire Go ecosystem working unchanged. If you've written Scala, Kotlin, F#, or OCaml and then had to go back to Go, you know exactly the gap GALA is trying to close.</p>
<pre><code class="language-gala">sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
}

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"
}
</code></pre>
<p>The rename isn't marketing varnish over the same code. It's a bar. "Scala on Go" sets an expectation — the type system should hold up across files and packages, the standard library should feel functional end to end, the tooling should exist — and the last eleven weeks were largely about earning it.</p>
<h2>The March roadmap, item by item</h2>
<p><strong>1. Stability and the type-inference regression suite — done, and then some.</strong> In March the type-inference regression suite had 14 cases. The bulk of the ~190 non-merge commits since are fixes and the regression guards that lock them in: cross-package <code>Try</code> chains, two-parameter <code>HashMap.ForEachKV</code> lambdas, tuple-literal element types driven by the call-site expected type, statement-position <code>match</code> lowering, sealed-case widening in tuple match arms. The pattern was consistent — every reported bug became a permanent example test before the fix shipped, per the project's own rule of never working around a transpiler bug.</p>
<p><strong>2. Cross-package generics — the rough edge, largely filed down.</strong> This was the honest weak spot in March, and it got the most attention. A few of the concrete fixes:</p>
<ul>
<li>Downward inference now flows into generic sealed-type case constructors across package boundaries.</li>
<li>Cross-package <code>Option</code>/<code>Try</code> <code>.Map</code> type parameters are now driven by the call-site expected type instead of being guessed locally.</li>
<li>Qualified type and function lookups are anchored to the qualifier's package, so multi-segment import paths resolve to the right symbol.</li>
</ul>
<p>Alongside the fixes, GALA gained <strong>coded diagnostics</strong>. Cross-package symbols now require an explicit import, and you get a real error code — <code>GALA-E0025</code> — instead of a vague resolution failure. Naming your errors is a small thing that signals a compiler growing up.</p>
<p><strong>3. IDE support — shipped, and wider than promised.</strong> March promised "a GoLand plugin for syntax highlighting and navigation." What shipped is that <em>plus</em> a full LSP server:</p>
<ul>
<li>A <strong>GoLand / IntelliJ plugin</strong> with an ANTLR-based parser, a complete PSI tree, semantic highlighting, structure view, and 12 live templates — all working locally without the language server.</li>
<li>An <strong>LSP server</strong> (<code>gala lsp</code>) that brings VS Code and Neovim along for the ride: real-time diagnostics (including match-exhaustiveness), hover with type signatures, go-to-definition across files, find references, type-aware completion, and inlay hints sourced from the transpiler's own <code>VarTypes</code>. It debounces analysis by 500ms so it stays quiet while you type.</li>
</ul>
<p><strong>4. Package registry — still in flight.</strong> This is the one item that didn't fully land. <code>gala mod add</code> handles Go and GALA dependencies today, including third-party Go modules, but a first-class public registry for publishing and discovering GALA libraries is still in progress. Calling it out for the same reason I called out generics in March: the honest list is the useful one.</p>
<h2>Four new standard-library packages</h2>
<p>The theme across the new packages is <strong>functional error handling reaching the edges of the library</strong> — the I/O-shaped corners that usually get a pass and return naked <code>(value, error)</code> or panic. In GALA they return <code>Try</code>.</p>
<p><strong><code>crypto</code></strong> — hashing (SHA-256/512, SHA-1, MD5), HMAC (SHA-256/512), Ed25519 signing, X25519 key exchange, and CSPRNG helpers. Hashes return a <code>Digest</code> you can render however you need:</p>
<pre><code class="language-gala">val digest = crypto.Sha256{}.Hash(data)   // data : Array[byte]
Println(digest.Hex())
Println(digest.Base64())
</code></pre>
<p>Crucially, the operations that <em>can</em> fail — random-byte generation, for instance — return <code>Try[Array[byte]]</code> rather than panicking, so a failed entropy read composes like any other fallible step instead of taking down the process.</p>
<p><strong><code>yaml</code></strong> — a type-safe YAML codec that shares the <em>exact same</em> <code>StructMeta[T]</code> compiler intrinsic as the JSON codec. Same zero-reflection machinery, same builder API, no struct tags:</p>
<pre><code class="language-gala">struct Project(Name string, Repo string, Port int)

val codec = Codec[Project](SnakeCase())
val text  = codec.Encode(Project("auth", ".", 8080)).Get()
// name: auth
// repo: .
// port: 8080
val back = codec.Decode(text)   // Try[Project]
</code></pre>
<p><strong><code>fs</code></strong> — file operations with <code>Try</code> return types throughout: <code>ReadFile</code>/<code>WriteFile</code>, their string variants, <code>ReadLines</code>/<code>WriteLines</code>/<code>AppendString</code> for line-oriented work, and directory operations <code>CopyFile</code>, <code>Rename</code>, <code>Walk</code>, and <code>Glob</code>.</p>
<pre><code class="language-gala">val saved = fs.WriteFileString("notes.txt", "hello", 0o644)
    .FlatMap((_) =&gt; fs.AppendString("notes.txt", "\nworld", 0o644))
    .FlatMap((_) =&gt; fs.ReadLines("notes.txt"))   // Try[Array[string]]
</code></pre>
<p><strong><code>path</code></strong> — a thin, ergonomic wrapper over <code>path/filepath</code>. Plus the <code>strings</code> package was rebuilt into an ergonomic free-function form, and <code>subprocess</code> was reworked to return explicit <code>Try</code> values instead of panicking on failure — the same edge-hardening pass that <code>crypto</code> got.</p>
<p>Concurrency picked up one notable primitive too: <code>Future.WithTimeout</code> is a monadic timeout that completes with the original result if it arrives in time and a <code>TimeoutError</code> otherwise.</p>
<pre><code class="language-gala">val result = slowCall().WithTimeout(Seconds(5)).Await()   // Try[T]
</code></pre>
<p>(It's honest about its limits: the timeout races the result, it doesn't <em>cancel</em> the underlying computation — the docstring says so plainly.)</p>
<h2>The build system grew a memory</h2>
<p>March's headline performance number was the 22x cold-compile speedup from batch transpilation. The work since has been about the <em>second</em> build and every build after it:</p>
<ul>
<li>A <strong>persistent-worker mode</strong> for the Bazel transpile action, so the analyzer process stays warm across builds instead of paying cold-start each time.</li>
<li>A <strong>hand-rolled binary codec</strong> replacing <code>gob</code> for the on-disk analysis cache, with the cache busted automatically when the <code>gala</code> binary's content changes.</li>
<li><strong>Memoization</strong> of per-package analysis, resolver lookups, and fingerprints across requests, plus parallel parsing of sibling files.</li>
</ul>
<p>The throughline is the same as the transpilation strategy itself: do the expensive analysis once, cache it honestly, and invalidate it correctly. We're also extracting the Bazel integration into a standalone <code>rules_gala</code> module so downstream projects can depend on it cleanly — that work is landing now.</p>
<h2>Dogfooding got ambitious</h2>
<p>In March the showcase was a web playground, a 7-file HTTP server, and a couple of demos. Two of the newer entries are a different order of magnitude, and both are written entirely in GALA:</p>
<ul>
<li><strong><a href="https://github.com/martianoff/gala-tui">GALA TUI</a></strong> — an Elm-architecture terminal-UI framework: immutable widgets, a differential renderer, an async runtime, a fuzzy command palette, markdown rendering, mouse support, and themes. It leans hard on sealed types for messages and pattern matching for the update loop — exactly the workload GALA was built for.</li>
<li><strong><a href="https://github.com/martianoff/gala-team">GALA Team</a></strong> — a multi-agent orchestrator where a Team Lead delegates to Engineers and QAs, reviews their work, and hands you a PR. A real concurrent program with real state, written in the language.</li>
</ul>
<p>These matter because they're the difference between "GALA is used somewhere in this project" and "this project <em>is</em> GALA." Frameworks shake out a language's ergonomics in a way that example programs never do.</p>
<h2>By the numbers</h2>
<table>
<thead>
<tr>
<th></th>
<th>March 2026</th>
<th>May 2026</th>
</tr>
</thead>
<tbody><tr>
<td>Version</td>
<td>0.25.2</td>
<td>0.50.0</td>
</tr>
<tr>
<td>Releases (total)</td>
<td>46</td>
<td>~101</td>
</tr>
<tr>
<td>New stdlib packages since</td>
<td>—</td>
<td>crypto, yaml, fs, path (+ rebuilt strings)</td>
</tr>
<tr>
<td>Type-inference regression cases</td>
<td>14</td>
<td>many dozens</td>
</tr>
<tr>
<td>IDE tooling</td>
<td>roadmap</td>
<td>GoLand plugin + LSP (VS Code, Neovim)</td>
</tr>
<tr>
<td>Tagline</td>
<td>Go Alternative Language</td>
<td>Scala on Go</td>
</tr>
</tbody></table>
<h2>What's still rough</h2>
<p>Same deal as last time — the honest list:</p>
<ul>
<li><strong>Package registry.</strong> Dependencies resolve, but there's no first-class registry for publishing and discovering GALA libraries yet. It's the top open roadmap item.</li>
<li><strong>Generics across packages.</strong> Much better than March, not <em>finished</em>. The common cases are solid and guarded by tests; the long tail of nested generic inference across module boundaries still surfaces the occasional sharp edge — and when it does, it becomes a repro test before it gets fixed.</li>
<li><strong>Maturity.</strong> GALA is four months old. It generates readable Go and runs real software, but it hasn't been load-bearing in a large team's production system for a year. Use it with eyes open.</li>
</ul>
<h2>The philosophy hasn't moved</h2>
<p>The thing I wrote in March still holds: the transpiler is honest about what it is — a source-to-source compiler that generates readable Go, with no special cases for its own standard library. What changed is the confidence behind the pitch. In March, "GALA is like Scala but for Go" was an aspiration. At 0.50.0 — with the type system holding across packages, an LSP in your editor, a functional standard library that reaches the I/O edges, and frameworks written in the language itself — it's closer to a description.</p>
<p><strong><a href="https://gala-playground.fly.dev">Try it in your browser</a></strong> — no install — or grab a binary from <a href="https://github.com/martianoff/gala/releases">Releases</a> and <code>gala run main.gala</code>.</p>
]]></content:encoded></item><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></channel></rss>