Type inference and annotations
How Nanyx infers types and when to annotate
Type inference is foundational in Nanyx; type annotations very rarely need to be written as the compiler infers types from values and usage. Annotations are, however, recommended for definitions exported from a module and strongly recommended for definitions exported from a package.
Inference by value
def message = "Hello world" -- inferred as string
def message: string = "Hello world" -- explicitly annotated
Inference for functions
def add = { a, b -> a + b }
-- inferred as (int, int) -> int from the usage of `+` on `a` and `b`
Since functions conceptually have exactly one parameter, the parameter types are inferred from how the function is used. If the function is never called, or if the parameter types cannot be inferred from the body of the function, then the parameter types will be inferred as generic type variables, and the function will be inferred as a generic function:
def identity = { x -> x }
-- inferred as `a -> a`, meaning it can take any type `a` and returns the same type `a`
Positional vs named inference
When functions are unannotated, Nanyx assumes that any deconstructed arguments are positional. If you want to use named parameters, annotate the function type:
-- Inferred as `(int, int) -> int` with positional parameters
def magnitude = { x, y -> math.sqrt(x * x + y * y) }
-- The annotation makes field names part of the function signature
def magnitude2: (x: int, y: int) -> float = { x, y -> math.sqrt(x * x + y * y) }
Field names in function signatures should be a deliberate choice, since callers must use the field names to call the function:
magnitude2(3, 4) -- ✗ This will not compile because the function expects named parameters
magnitude2(x = 3, y = 4) -- The correct way to call a function with named parameters
This is the one rule for named fields everywhere in the language: it applies equally when constructing a record from a type alias (see Constructing records from a type alias), so Point(x = 3, y = 4) is correct and Point(3, 4) is an error for a type with named fields.
Also, when fields in a function signature are named—as with any kind of deconstruction—deconstruction must also be by name in the function body (although if the deconstructed names are the same as the field names, you can collapse field = field to just field):
def magnitude2: (x: int, y: int) -> float = { x, y -> math.sqrt(x * x + y * y) }
-- or
def magnitude3: (x: int, y: int) -> float
= { x = innerX, y = innerY ->
math.sqrt(innerX * innerX + innerY * innerY)
}
Exported values
By convention, exported values should be annotated:
export def add = { a, b -> a + b }
-- Analyzer warning: missing type annotation
export def add: (int, int) -> int = { a, b -> a + b }
Although the language itself doesn’t mandate this practice, the official style analyzer will emit a warning if this is not followed.
Target typing
Nanyx can infer types based on how values are used:
def parse: string -> a = { json -> ... }
def obj = parse("{}")
def person: Person = obj
You can also use assertions to guide inference:
def obj = parse("..."): Person