Related pages: Why GALA? Code Examples Immutable Collections Mutable Collections Streams Strings Time Utilities Dependency Management

GALA Language Specification

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.


Table of Contents

  1. Project Structure
  2. Variable Declarations
  3. Functions
  4. Types and Structs
  5. Interfaces
  6. Control Flow
  7. Functional Features
  8. Generics
  9. Standard Library Types
  10. Literals and Type Conversions
  11. Go Built-in Functions
  12. Immutability Under the Hood
  13. GALA Packages
  14. Embedding Files
  15. Testing
  16. Best Practices
  17. Dependency Management
  18. Further Reading
  19. IDE Support

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.


1. Project Structure {#1-project-structure}

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"
)

Multi-File Packages

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"],
)

2. Variable Declarations {#2-variable-declarations}

GALA distinguishes between immutable and mutable variables.

Immutable (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

Mutable (var)

Variables declared with var are mutable and can be reassigned.

var y = 20
y = 30 // OK

Short Variable Declaration

Inside functions, := declares immutable variables.

func main() {
    z := 40
    // z = 50 // Compile error: cannot assign to immutable variable
}

3. Functions {#3-functions}

GALA supports both Go-style block functions and Scala-style expression functions.

Block Functions

func add(a int, b int) int {
    return a + b
}

Expression Functions

func square(x int) int = x * x

Parameters

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
}

Named Arguments

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.

Default Parameter Values

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:

Higher-Order Functions

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
}

By-Name Arguments (Thunk Sugar)

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.

Variadic Functions

func sum(numbers ...int) int {
    var total = 0
    var i = 0
    for ; i < len(numbers) ; {
        total = total + numbers[i]
        i = i + 1
    }
    return total
}

Methods

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))

4. Types and Structs {#4-types-and-structs}

Shorthand Struct Declaration

struct Person(name string, age int, var score int)

Block Struct Declaration

type Person struct {
    Name string    // Immutable
    age  int       // Immutable
    var Score int  // Mutable
}

Struct Construction

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

Automatic Copy and Equal Methods

val p1 = Person("Alice", 30)
val p2 = p1.Copy(age = 31) // p2 is Person("Alice", 31)
val same = p1.Equal(p2)    // false

Sealed Types (Algebraic Data Types)

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)
}

5. Interfaces {#5-interfaces}

type Shaper interface {
    Area() float64
}

struct Rect(width float64, height float64)
func (r Rect) Area() float64 = r.width * r.height

6. Control Flow {#6-control-flow}

If Statement and Expression

val status = if (score > 50) "pass" else "fail"

Match Expression

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"
}

Type-Based Pattern Matching

val res = x match {
    case s: string => s"Found string: $s"
    case i: int    => s"Found int: $i"
    case _         => "Unknown type"
}

Extractors and Unapply

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"
}

Pattern Matching Filters (Guards)

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"
}

Sequence Pattern Matching

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"
}

Monadic Binding (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 Statement

for i := 0; i < 10; i++ {
    Println(i)
}

for count < 5 {
    count++
}

for _, v := range items {
    Println(v)
}

7. Functional Features {#7-functional-features}

Lambda Expressions

val f = (x int) => x * x

// Inferred parameter types
val doubled = opt.Map((x) => x * 2)

// Void closures
opt.ForEach((x) => { Println(x) })

Partial Function Literals

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 })

8. Generics {#8-generics}

func identity[T any](x T) T = x

type Box[T any] struct { Value T }

9. Standard Library Types {#9-standard-library-types}

Option Monad

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)

Tuple

val t = (1, "hello")
val (a, b) = t         // a = 1, b = "hello"

Either

val e = Right[int, string]("success")
val msg = e match {
    case Left(code) => s"Error code: $code"
    case Right(s)   => "Result: " + s
}

Try Monad

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()}"
}

Future Monad

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)

Slices and Maps (Go Interop)

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)

10. Literals and Type Conversions {#10-literals-and-type-conversions}

String Interpolation

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

Rune Literals

val asterisk = '*'
val space = ' '
val newline = '\n'

Type Conversions

val n = int64(42)
val f = float64(10)
val r = rune(65)            // 'A'
val s = string(r)           // "A"

11. Go Built-in Functions {#11-go-built-in-functions}

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.

12. Immutability Under the Hood {#12-immutability-under-the-hood}

Pointer Types and Immutability

var data = 42
val ptr1 = &data     // immutable pointer binding
*ptr1 = 100          // OK: can modify data through pointer
// ptr1 = &other     // ERROR: cannot reassign immutable pointer

ConstPtr - Read-Only Pointers

val data = 42
val ptr = &data  // ptr is ConstPtr[int]
val value = *ptr // OK: read
// *ptr = 100    // ERROR: cannot write through ConstPtr

13. GALA Packages {#13-gala-packages}

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
}

Package Visibility

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.

14. Embedding Files {#14-embedding-files}

embed val readme = "README.md"              // string
embed val static EmbeddedFS = "static/*"    // EmbeddedFS

val content = static.ReadString("static/index.html")

15. Testing {#15-testing}

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)
}

Table-Driven Tests

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),
    )
}

Benchmarking

func main() {
    RunBenchmarks(
        BenchFunc(Name = "BenchmarkAdd", Func = (b B) => {
            for i := 0; i < b.N; i++ {
                val x = 1 + 1
            }
        }),
    )
}

16. Best Practices {#16-best-practices}

17. Dependency Management {#17-dependency-management}

GALA 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

18. Further Reading {#18-further-reading}

19. IDE Support {#19-ide-support}

IntelliJ IDEA

A basic IntelliJ IDEA plugin is available in ide/intellij.

  1. Build: bazel build //ide/intellij:plugin
  2. Install: Settings > Plugins > Install Plugin from Disk…

The 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.