Type Inference — Less Typing, Same Safety

GALA is statically typed, but you rarely need to write type annotations. The compiler infers variable types, lambda parameter types, generic type arguments, and accumulator types from context. The result is code that reads like a dynamic language but compiles with full type safety.

val nums = ArrayOf(1, 2, 3, 4, 5)
val doubled = nums.Map((x) => x * 2)
val sum = nums.FoldLeft(0, (acc, x) => acc + x)

In the snippet above, GALA infers:

No type annotations needed. No any or interface{} in the generated Go code.


Variable Type Inference

The type of val and var bindings is inferred from the right-hand side:

val x = 42                  // int
val name = "Alice"          // string
val pi = 3.14159            // float64
val active = true           // bool
val pair = Tuple(1, "two")  // Tuple[int, string]
val opt = Some(42)          // Option[int]

You can add an explicit type annotation when needed — for example, to assign to an interface type or to use a wider type:

val x float64 = 42          // float64 (not int)
val s Shaper = Circle(5.0)  // interface type

Lambda Parameter Type Inference

When a lambda is passed to a method with known parameter types, the lambda’s parameter types are inferred from the method signature. This is GALA’s most impactful inference feature — it eliminates the most common type annotations in functional code.

How It Works

// The compiler knows Array[int].Map takes func(int) U
// So (x) must be int
val doubled = ArrayOf(1, 2, 3).Map((x) => x * 2)

// The compiler knows Array[int].Filter takes func(int) bool
// So (x) must be int
val evens = ArrayOf(1, 2, 3).Filter((x) => x % 2 == 0)

Supported Contexts

Lambda parameter inference works in these contexts:

Generic receiver types — Methods on Array[T], List[T], Option[T], HashMap[K,V], and other generic types. The receiver’s type parameters are resolved and substituted into the method signature:

val opt = Some(42)
val doubled = opt.Map((x) => x * 2)         // x inferred as int
opt.ForEach((x) => { Println(x) })          // x inferred as int
val positive = opt.Filter((x) => x > 0)     // x inferred as int

Non-generic wrapper types — Methods on concrete types that take function parameters:

val s = S("hello")
val upper = s.Map((r) => r - 32)             // r inferred as rune
val hasVowel = s.Exists((r) => r == 'a')     // r inferred as rune

Free function calls — Lambda parameters are also inferred when passed to generic free functions:

val result = identity((x) => x * 2)

Generic Method Type Parameter Inference

When you call a generic method, GALA infers the method’s type parameters from the concrete argument types. You almost never need to write them explicitly:

// Map[U] — U is inferred as int from the lambda return type
val doubled = ArrayOf(1, 2, 3).Map((x) => x * 2)

// Instead of the explicit form:
// val doubled = ArrayOf(1, 2, 3).Map[int]((x int) => x * 2)

This works for all generic methods including Map, FlatMap, Filter, FoldLeft, Zip, Collect, and more.


FoldLeft Accumulator Inference

The accumulator type parameter U in FoldLeft[U](zero U, f func(U, T) U) is inferred from the zero value argument:

val nums = ArrayOf(1, 2, 3)

// acc is int because the zero value is 0 (int)
val sum = nums.FoldLeft(0, (acc, x) => acc + x)

// acc is string because the zero value is "" (string)
val csv = nums.FoldLeft("", (acc, x) => acc + s"$x,")

// acc is float64 because the zero value is 0.0 (float64)
val avg = nums.FoldLeft(0.0, (acc, x) => acc + float64(x))

No explicit type annotation on the accumulator parameter, the zero value, or the generic type parameter.


Generic Constructor Inference

When constructing generic types, type parameters are inferred from the arguments:

// Inferred: Some[int]
val x = Some(42)

// Inferred: Right[string, int]
val r = Right[string, int](42)

// Inferred: Tuple[int, string]
val t = Tuple(1, "hello")

// Inferred: ListOf creates List[int]
val list = ListOf(1, 2, 3)

// Inferred: HashMapOf creates HashMap[string, int]
val m = HashMapOf(("a", 1), ("b", 2))

Write Some(42), not Some[int](42). Write ListOf(1, 2, 3), not ListOf[int](1, 2, 3).


Sealed Type Constructor Inference

Sealed type variant constructors infer their types from the arguments:

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

val c = Circle(5.0)          // Shape, no type annotation needed
val r = Rectangle(3.0, 4.0)  // Shape

What Is NOT Inferred

GALA does not infer types in these contexts — you must provide explicit annotations:

Top-level function parameters and return types:

// Parameters MUST have type annotations
func add(a int, b int) int = a + b

// Return type MUST be specified
func greet(name string) string = s"Hello, $name"

Interface implementations — when a value needs to satisfy an interface, you may need to annotate the variable:

val s Shaper = Circle(5.0)  // explicit interface type

Ambiguous literals — when the compiler cannot determine which numeric type you mean:

val x float64 = 42  // 42 would default to int without annotation

Best Practices: When to Annotate vs When to Omit

Context Recommendation
val x = 42 Omit — inferred as int
val x = Some(42) Omit — inferred as Option[int]
Lambda parameters in pipelines Omit — inferred from method signature
FoldLeft accumulator Omit — inferred from zero value
Generic method type params Omit — inferred from arguments
Function parameters Annotate — always required
Function return types Annotate — always required
Interface variable assignment Annotate — needed for interface dispatch
Wider numeric types Annotate — val x float64 = 42

The general rule: omit types inside function bodies, annotate types at function boundaries.


Two-Layer Inference Architecture

Under the hood, GALA uses a two-layer inference system:

  1. Layer 1 (Pattern-based) — Fast inference that handles 90%+ of cases: literals, scope lookups, function call return types, struct field types, and operator results.

  2. Layer 2 (Hindley-Milner) — Full unification-based inference for complex cases: generic function instantiation, lambda parameter inference, and polymorphic type schemes.

The compiler tries Layer 1 first for speed. If it returns an unresolved type (containing type parameters like T), Layer 2 takes over with Algorithm W unification. This two-layer approach keeps compile times fast while handling complex generic code correctly.


Further Reading