Types

Invisible types, ADTs, and what the compiler infers.

Invisible Types

Loon is statically typed, but you'd be forgiven for not noticing. The compiler uses Hindley-Milner type inference to figure out every type from context. You never write type annotations. You never fight with the type checker. You just write code, and the compiler verifies that it all makes sense.

[let x 42]              ; Int
[let name "Loon"]       ; Str
[let nums #[1 2 3]]     ; Vec Int
[fn add [a b] [+ a b]]  ; Int -> Int -> Int

The compiler sees [+ a b] and knows that + works on Int values, so a and b must be Int, and the return type must be Int too. All of that happens without you lifting a finger.

What Gets Inferred

The inference engine doesn't just handle simple cases. It works through function calls, generics, closures, and higher-order functions. Here's a function that composes two functions together:

[fn compose [f g]
  [fn [x] [f [g x]]]]
; (b -> c) -> (a -> b) -> (a -> c)

[let inc-dbl [compose
  [fn [n] [+ n 1]]
  [fn [n] [* n 2]]]]

The inferred type of compose is fully generic: it takes any function from b to c, any function from a to b, and returns a function from a to c. When you actually call compose with concrete functions, the type variables get pinned to specific types. No annotations required.

Viewing Types

Since you never write types, you might wonder how to see them. The language server shows inferred types on hover and as inlay hints in your editor. You get all the benefits of explicit types (documentation, error catching) without any of the annotation burden.

Install the Loon LSP extension to see inferred types inline in your editor.

Algebraic Data Types

When you need to model data that can be one of several shapes, you define an algebraic data type (ADT) with type. Each variant is a constructor. Variants can hold data or stand alone.

[type Shape
  [Circle Float]
  [Rect Float Float]
  Point]

[let s [Circle 5.0]]

Circle holds a Float (the radius). Rect holds two Float values (width and height). Point holds nothing. You construct values by calling the variant like a function. Later, you'll use match to take them apart. This is the core of how Loon models domain data: you define the shapes, and the compiler makes sure you handle all of them.

Option and Result

Loon has no nil and no null. If something might not exist, you use Option. If something might fail, you use Result. These are just regular ADTs that happen to be built in, and they force you to handle the absent or error case explicitly.

; Option is [Some value] or None
[let found [Some 42]]
[let missing None]

; Result is [Ok value] or [Err msg]
[let ok [Ok 100]]
[let bad [Err "not found"]]

This might seem annoying at first if you're used to just returning null, but it eliminates null pointer exceptions entirely. Every place where a value might be absent is visible in the type, and the compiler won't let you forget to check.

There is no nil/null in Loon. Use None or the empty string as defaults.

Generics

Generic functions happen automatically. If the compiler can't pin a parameter down to a specific type, it gives it a type variable, which means the function works for any type.

[fn identity [x] x]
; a -> a

[fn first [pair]
  [get pair 0]]
; Vec a -> a

identity takes any value and returns it unchanged. Its type is a -> a, where a can be anything. first takes a vector of any type and returns an element of that type. You didn't ask for generics; the compiler figured it out from the code.

Type Signatures

You can optionally add a type signature with sig if you want to document a function's type or constrain what the compiler infers. This is never required. Think of it as a form of documentation that the compiler checks for you.

[sig add : Int -> Int -> Int]
[fn add [a b] [+ a b]]

If the signature doesn't match what the compiler infers from the body, you'll get a type error. This makes sig useful as an assertion: "I intend this function to have this type, tell me if I'm wrong."

Row Polymorphism

This one is subtle but powerful. When a function accesses a key on a map, the compiler infers that the function needs a map with at least that key. It doesn't care what other keys are present. This is called row polymorphism, and it means your functions are automatically as flexible as they can be.

[fn get-name [user]
  [get user :name]]

; Works with any map containing :name
[get-name {:name "Ada" :age 30}]
[get-name {:name "Alan" :role "CS"}]

Both calls work because both maps have a :name key. The first also has :age and the second has :role, but get-name doesn't care about those. You get structural typing for maps without any special syntax or interface declarations.

Dimensional Types

Loon has compile-time dimensional analysis. Write numbers with unit suffixes and the type checker tracks physical dimensions through arithmetic. If you try to add meters to seconds, it is a type error. Zero runtime overhead.

[let d 100.0m]              ; Length
[let t 9.58s]               ; Time
[let v [/ d t]]             ; Velocity
[let f [* 80.0kg [/ d [* t t]]]]  ; Force

Unit suffixes like m, s, kg desugar to calls to the unit builtin at parse time. So 5.0m is exactly [unit 5.0 :m]. There are 30+ recognized units with SI prefixes.

The type checker knows 21 named physical quantities. When you hover over a variable in the LSP, you see Velocity or Force rather than raw dimension exponents.

No Dimensionless

Just as Loon has no null, it has no silent escape from the physics type world. When you divide meters by meters, you get Scalar — not Float. To get a raw float, you must explicitly call magnitude. To enter the physics world from a float, call scalar.

[/ 10.0m 5.0m]        ; Scalar (NOT Float)
[magnitude 10.0m]     ; 10.0 : Float (explicit exit)
[scalar 2.0]          ; Scalar (explicit entry)

Float literals can still multiply/divide dimensional values as scalar multipliers. The no-dimensionless rule only applies to the output of dimensional arithmetic.

Physics as Effects

Physical constants and material properties are algebraic effects. Swap assumptions by changing handlers. This lets the same analysis code run with different materials, gravity, or simulation backends.

[handle [design-beam 10.0kN 5.0m]
  [Physics.yield-strength] [resume 250.0MPa]
  [Physics.gravity] [resume [unit 9.81 :m]]]

The Physics effect provides gravity, yield-strength, elastic-modulus, density, temperature, and thermal-conductivity. The Sim effect provides stress, deflection, natural-freq, and thermal-field.