Types

Invisible types, ADTs, and what the compiler infers.

Invisible Types

Loon is statically typed, and you'd be forgiven for not noticing. A Hindley-Milner inference engine recovers every type from context, so you never write an annotation and never argue with the checker. You write the code; the compiler quietly proves it holds together.

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

Seeing [+ a b], the compiler reasons that + demands Int values, so a and b are Int, and the result is Int as well. The whole chain of deduction runs while you do nothing at all.

What Gets Inferred

Inference is not confined to the easy cases. It reasons through function calls, generics, closures, and higher-order functions. Consider a function that composes two functions:

[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: a function from b to c, a function from a to b, and a function from a to c out the other end. Call it with concrete functions and the type variables snap to specific types. You wrote no annotations to make this happen.

Viewing Types

If you never write types, how do you read them? The language server surfaces every inferred type on hover and as inlay hints, right in your editor. The documentation and the error-catching of explicit types are yours, with none of the typing.

Tip

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

Algebraic Data Types

Some data refuses to be one fixed shape; it is a circle or a rectangle, present or absent, this case or that. You model it with an algebraic data type (ADT), declared with type. Each variant is a constructor, and a variant may carry 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 build a value by calling its variant like a function, and later you reach for match to take it apart. This is how Loon models a domain: you name the shapes, and the compiler holds you to handling every one.

Option and Result

Loon has no nil and no null. When a value might not exist, you reach for Option; when an operation might fail, for Result. They are ordinary ADTs that happen to ship in the prelude, and they make the absent case and the error case impossible to overlook.

; 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"]]

Coming from a language where you return null, the ceremony can grate at first. The payoff is total: null pointer exceptions cannot occur. Every place a value might be missing is written into the type, and the compiler will not let the check slip your mind.

Note

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

Generics

Generic functions arrive on their own. When the compiler cannot pin a parameter to a specific type, it hands the parameter a type variable, and the function works for every type at once.

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

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

identity takes any value and hands it back untouched. Its type is a -> a, where a stands for anything. first takes a vector of any type and returns an element of that type. You never asked for generics; the compiler read them out of the code.

Type Signatures

You may add a type signature with sig to document a function's type or to constrain what inference is allowed to conclude. It is never required. Read it as documentation the compiler keeps honest.

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

If the signature disagrees with what the body implies, you get a type error. That makes sig a quiet assertion: "I mean this function to have this type, tell me if I'm wrong."

Row Polymorphism

Here is a subtle idea with outsized reach. When a function reads a key from a map, the compiler infers only that the map must carry at least that key; the rest of the map is none of the function's business. This is row polymorphism, and it makes your functions as accommodating as they can safely be, without a word from you.

[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 succeed because both maps carry a :name key. The first also has :age and the second has :role, and get-name is indifferent to either. This is structural typing for maps, with no special syntax and no interface to declare.

Dimensional Types

Loon performs dimensional analysis at compile time. Tag a number with a unit suffix and the type checker carries its physical dimension through every operation. Add meters to seconds and the program will not compile. None of this costs a thing at runtime.

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

A unit suffix like m, s, or kg desugars at parse time into a call to the unit builtin, so 5.0m is precisely [unit 5.0 :m]. More than 30 units are recognized, each with SI prefixes.

The type checker names 21 physical quantities. Hover over a variable in the LSP and you read Velocity or Force, not a row of dimension exponents.

No Dimensionless

Just as Loon offers no silent null, it offers no silent escape from the world of physical types. Divide meters by meters and you get a Scalar, not a Float. To reach a raw float you call magnitude out loud; to step from a float back into physics you call scalar. The boundary is always crossed on purpose.

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

Note

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

Physics as Effects

Physical constants and material properties are not baked in; they are algebraic effects. Change a handler and you change an assumption, which lets one piece of analysis code run against different materials, a different gravity, or a different simulation backend.

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

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