| Related pages: Why GALA? | Code Examples | Immutable Collections | Mutable Collections | Streams | Strings | Time Utilities | Dependency Management |
GALA (Go Alternative LAnguage) is a modern programming language that transpiles to Go. It combines Go’s efficiency and simplicity with features inspired by Scala and other functional languages, such as immutability by default, pattern matching, and concise expression syntax.
Note: This is the complete language specification. For the full, continuously updated source, see the GALA.MD file on GitHub. The content below is a faithful copy of that specification.
GALA files use the .gala extension. Every file must start with a package declaration, followed by an empty line. All GALA files in the same directory must belong to the same package.
GALA supports Go-style imports, including aliases and dot imports. Import declarations must also be followed by an empty line.
package main
import (
"fmt"
m "math"
. "net/http"
)
A GALA package can span multiple .gala files. Types, sealed types, and functions defined in one file are available in all other files of the same package. Each file must declare the same package name.
shapes/
types.gala # struct Point, sealed type Shape
ops.gala # func (p Point) String(), func Describe(s Shape)
In Bazel, list all source files in srcs:
gala_library(
name = "shapes",
srcs = ["shapes/types.gala", "shapes/ops.gala"],
importpath = "myapp/shapes",
)
gala_binary(
name = "app",
srcs = ["main.gala", "helpers.gala"],
)
GALA distinguishes between immutable and mutable variables.
val)Variables declared with val are immutable. They must be initialized at declaration time.
val x = 10
val a, b = 1, 2
// x = 20 // Compile error: cannot assign to immutable variable
var)Variables declared with var are mutable and can be reassigned.
var y = 20
y = 30 // OK
Inside functions, := declares immutable variables.
func main() {
z := 40
// z = 50 // Compile error: cannot assign to immutable variable
}
GALA supports both Go-style block functions and Scala-style expression functions.
func add(a int, b int) int {
return a + b
}
func square(x int) int = x * x
Function parameters can be marked as val or var. By default, they are val (immutable).
func process(val data string, var count int) {
// data = "new" // Error
count = count + 1 // OK
}
Function calls support named arguments. Named arguments can appear in any order — the compiler reorders them to match the function signature.
func divide(dividend int, divisor int) int = dividend / divisor
divide(divisor = 4, dividend = 20) // 5 — reordered to divide(20, 4)
divide(20, 4) // 5 — positional works too
Named arguments work with struct construction, Copy() method overrides, and regular function calls.
Parameters can have default values. When a function is called without providing a defaulted argument, the default expression is injected at the call site. Default parameters must come after required parameters.
func connect(host string, port int = 8080, tls bool = true) {
Println(s"Connecting to $host:$port (tls=$tls)")
}
connect("localhost") // port=8080, tls=true
connect("localhost", 3000) // tls=true
connect("localhost", 3000, false) // all explicit
// Named arguments + defaults: skip any defaulted parameter
connect("localhost", tls = false) // port=8080
connect(host = "localhost", port = 443) // tls=true
Default expressions are evaluated at each call site (not once at definition time):
func log(msg string, ts time.Time = time.Now()) {
Println(s"[$ts] $msg")
}
Expression functions also support defaults:
func greet(name string, greeting string = "Hello") string = s"$greeting, $name!"
The compiler validates defaults at compile time:
Functions are first-class values:
func apply(x int, f func(int) int) int = f(x)
func main() {
val double = (x int) => x * 2
val result = apply(5, double) // result = 10
}
When a parameter’s expected type is a zero-arg function type (func() T or
void func()), you can pass a plain expression where a lambda is expected — it
is automatically lifted into () => expr and evaluated lazily each time the
thunk is called. This is what lets Try and Future read like direct calls:
func runTwice(body func() int) int = body() + body()
func main() {
val a = runTwice(compute()) // sugar — lifted to () => compute()
val b = runTwice(() => compute()) // explicit form; identical
val f = Future(loadFromDB(id)) // same as Future(() => loadFromDB(id))
val r = Try(strconv.Atoi("42")) // same as Try(() => strconv.Atoi("42"))
}
The type parameter is inferred from the lifted expression’s own type
(Future(compute()) where compute() int yields Future[int]). The lift is
purely additive: an argument that is already a function value is passed through
untouched, and multi-argument function parameters still require an explicit lambda.
func sum(numbers ...int) int {
var total = 0
var i = 0
for ; i < len(numbers) ; {
total = total + numbers[i]
i = i + 1
}
return total
}
type Box[T any] struct { Value T }
func (b Box[T]) GetValue() T = b.Value
func (b Box[T]) Transform[U any](f func(T) U) Box[U] = Box[U](Value = f(b.Value))
struct Person(name string, age int, var score int)
type Person struct {
Name string // Immutable
age int // Immutable
var Score int // Mutable
}
val p1 = Person{Name: "Alice", Age: 30} // Named fields (Go-style)
val p2 = Person("Bob", 25) // Positional (Functional-style)
val p3 = Person(age = 20, name = "Charlie") // Named arguments
val p1 = Person("Alice", 30)
val p2 = p1.Copy(age = 31) // p2 is Person("Alice", 31)
val same = p1.Equal(p2) // false
sealed type Shape {
case Circle(Radius float64)
case Rectangle(Width float64, Height float64)
case Point()
}
val c = Circle(3.14)
val desc = c match {
case Circle(radius) => f"radius=$radius%.2f"
case Rectangle(w, h) => f"$w%fx$h%f"
case Point() => "point"
}
Generic sealed types:
sealed type Result[T any] {
case Ok(Value T)
case Err(Error error)
}
type Shaper interface {
Area() float64
}
struct Rect(width float64, height float64)
func (r Rect) Area() float64 = r.width * r.height
val status = if (score > 50) "pass" else "fail"
val result = x match {
case 1 => "one"
case 2 => "two"
case n => s"Value is $n"
case _ => "other"
}
// Boolean exhaustive match
val desc = flag match {
case true => "enabled"
case false => "disabled"
}
val res = x match {
case s: string => s"Found string: $s"
case i: int => s"Found int: $i"
case _ => "Unknown type"
}
type Even struct {}
func (e Even) Unapply(i int) Option[int] = if (i % 2 == 0) Some(i) else None[int]()
42 match {
case Even(n) => s"$n is even"
case _ => "odd"
}
val res = x match {
case i: int if i > 100 => "Large integer"
case Person(name, age) if age < 18 => name + " is a minor"
case _ => "Other"
}
val arr = ArrayOf(1, 2, 3, 4, 5)
val res = arr match {
case Array(head, tail...) => s"Head: $head, Tail size: ${tail.Size()}"
case _ => "Empty"
}
bind / also)Inside a function whose result type is a monad M, bind name = expr unwraps M and binds the success value; the block lowers to a FlatMap chain, so every bound name stays in scope for later statements. also marks an independent clause: a bind+also group lowers through Zip{N} when the monad provides it — Validated (in the validation package) accumulates all errors, Future runs the clauses concurrently — and falls back to the sequential FlatMap chain for fail-fast monads (Try/Option/Either).
// bind: each value stays in scope; the first Failure short-circuits.
func processOrder(id int) Try[Receipt] {
bind o = fetchOrder(id)
bind valid = validateOrder(o)
bind payment = chargePayment(valid)
Success(Receipt(o.Id, payment))
}
// also over Validated: report ALL invalid fields, not just the first.
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))
}
Bound names are immutable vals. bind/also work over any user-defined monad that defines FlatMap[U](f func(T) M[U]) M[U]; a type without FlatMap is rejected with a clear compiler error. See bind / also notation for the full specification and the user-monad extension guide.
for i := 0; i < 10; i++ {
Println(i)
}
for count < 5 {
count++
}
for _, v := range items {
Println(v)
}
val f = (x int) => x * x
// Inferred parameter types
val doubled = opt.Map((x) => x * 2)
// Void closures
opt.ForEach((x) => { Println(x) })
val pf = { case 1 => "one" case 2 => "two" }
val r1 = pf(1) // Some("one")
val r2 = pf(5) // None[string]
// Use with Collect
val evenDoubled = numbers.Collect({ case n if n % 2 == 0 => n * 2 })
func identity[T any](x T) T = x
type Box[T any] struct { Value T }
val x = Some(10)
val y = None[int]()
val msg = x match {
case Some(v) => s"Got: $v"
case None() => "Empty"
}
val result = x.Map((i) => i * 2)
val value = x.GetOrElse(0)
val t = (1, "hello")
val (a, b) = t // a = 1, b = "hello"
val e = Right[int, string]("success")
val msg = e match {
case Left(code) => s"Error code: $code"
case Right(s) => "Result: " + s
}
val result = Try(riskyDivide(10, 0)) // by-name sugar: runs () => riskyDivide(...) lazily
val parsed = Try(strconv.Atoi("42")) // Go (T, error) auto-wrapped
val dir = Try(os.TempDir) // function reference sugar (bare zero-arg call)
val msg = result match {
case Success(n) => s"Got: $n"
case Failure(e) => s"Error: ${e.Error()}"
}
import . "martianoff/gala/concurrent"
val async = Future[int](expensiveComputation()) // by-name: runs () => expensiveComputation() async
val result = async.Await() // Returns Try[int]
val doubled = async.Map((v) => v * 2)
Prefer GALA collections over Go slices for most use cases. See Immutable Collections.
import . "martianoff/gala/collection_immutable"
import . "martianoff/gala/go_interop"
// PREFERRED: GALA collections
val nums = ArrayOf(1, 2, 3, 4, 5)
val doubled = nums.Map((x) => x * 2)
// GO INTEROP: When you need []T (SliceOf lives in go_interop)
val goSlice = SliceOf(1, 2, 3)
val name = "Alice"
val age = 30
Println(s"Hello $name!") // Hello Alice!
Println(s"$name is $age years old") // Alice is 30 years old
Println(s"Next year: ${age + 1}") // Next year: 31
Println(s"Price: $$99") // Price: $99
// Explicit format specs
Println(f"$count%04d items") // 0007 items
Println(f"Total: $$$price%.2f") // Total: $19.99
val asterisk = '*'
val space = ' '
val newline = '\n'
val n = int64(42)
val f = float64(10)
val r = rune(65) // 'A'
val s = string(r) // "A"
Go’s built-in functions are available: len, cap, make, new, append, delete, close, panic, recover.
For most use cases, prefer GALA’s collection types and their methods over Go built-ins.
var data = 42
val ptr1 = &data // immutable pointer binding
*ptr1 = 100 // OK: can modify data through pointer
// ptr1 = &other // ERROR: cannot reassign immutable pointer
val data = 42
val ptr = &data // ptr is ConstPtr[int]
val value = *ptr // OK: read
// *ptr = 100 // ERROR: cannot write through ConstPtr
GALA supports Go-style imports with aliases and dot imports.
import m "math"
import . "martianoff/gala/examples/mathlib"
func main() {
val res = m.Sqrt(16.0)
val sum = Add(10, 20) // from mathlib via dot import
}
Visibility is controlled at two levels. Identifiers use casing, as in Go: a PascalCase type, function, field or method is exported from its package; a camelCase one is not.
Packages use internal directories. A package under a directory named internal is importable only from the tree rooted at that directory’s parent — Go’s rule, applied to GALA import paths:
mylib/
mylib.gala package mylib — public
internal/
detail/detail.gala package detail — private to example.com/mylib
sub/
internal/
deep/deep.gala package deep — private to example.com/mylib/sub
a/b/c/internal/d is importable only from the tree rooted at a/b/c. Only whole path elements count — a directory named internalize is an ordinary public package. There is no exemption for the standard library.
This lets a library share code across its own packages without adding it to the API it must keep supporting: consumers call what the parent package exports and cannot reach internal/detail, so it can be renamed or deleted freely. A forbidden import is rejected with GALA-E0041.
Publishing an internal package means moving it out of the internal directory, which changes its import path — so it is worth deciding deliberately. For an application, the whole module is already private to you; reach for internal where there is a real API boundary to defend, not as a default layout.
See Dependency Management for managing external packages.
embed val readme = "README.md" // string
embed val static EmbeddedFS = "static/*" // EmbeddedFS
val content = static.ReadString("static/index.html")
GALA provides a test framework with 22 assertions, panic recovery, timing, table-driven test support, and benchmarking.
package main
import . "martianoff/gala/test"
func TestAddition(t T) T {
val x = 1 + 1
return Eq(t, x, 2)
}
func TestDouble(t T) T {
return RunCases[int, int](t,
(sub T, input int, expected int) => Eq(sub, input * 2, expected),
Case[int, int](Name = "zero", Input = 0, Expected = 0),
Case[int, int](Name = "positive", Input = 5, Expected = 10),
)
}
func main() {
RunBenchmarks(
BenchFunc(Name = "BenchmarkAdd", Func = (b B) => {
for i := 0; i < b.N; i++ {
val x = 1 + 1
}
}),
)
}
val over var - Use mutable variables only when necessaryCopy() for updates - person.Copy(age = 31)match over if-else chains for type/value dispatchcase Some(x) => not if opt.IsDefined() { x := opt.Get() }Some(42) not Some[int](42)s"..." over fmt.Sprintf - s"Hello $name" not fmt.Sprintf("Hello %s", name)Array or List from collection_immutableOption[T] for nullable values, Try[T] for operations that may failGALA provides a module system similar to Go modules. See Dependency Management for the full guide.
gala mod init github.com/user/project
gala mod add github.com/example/utils@v1.2.3
gala mod add github.com/google/uuid@v1.6.0 --go
gala mod tidy
string, Str wrapper, and StringBuilderA basic IntelliJ IDEA plugin is available in ide/intellij.
bazel build //ide/intellij:pluginThe plugin provides syntax highlighting, file type recognition (.gala), and basic code structure support.
Full specification: This page contains a summary of the GALA language specification with all major features documented. For the complete, unabridged specification with every detail and edge case, see the GALA.MD source file on GitHub.