Narrowing › function. The names in the examples are those of section 2.
A function type is {:function, parameters, result}: function<number> -> number is a function of one number that gives a number or null. Parameter names are not part of the type, and a function has no value set. A fact about a function can say only whether it is null, and which function types it is an instance of: a function is the function types it was declared with, and f instance of T asks whether one of them is within T (Feel.Type.Arrows, D-171). The internal :function kind holds every function regardless of its parameters. It appears in remainders of Any and as a fallback when erasing an arrow would strengthen its domain; it has no declared FEEL syntax. A function type minus another is a type (D-152), and is empty when a function type of the function is within the other; a built-in function with several signatures is all of them, an overload (overloaded functions).
A function is written function<A> -> R | Null below, the last | Null being the function's own: a name of a function type can be null like any other.
Recorded arrows guarantee signatures without excluding others, including signatures with the same arity and different parameter domains. A positional call outside every recorded domain starts at Any | Null, then applicable call refinements narrow it. A declaration alone cannot prove that the call fails. Known literal functions carry their syntax separately. Feel.Recursion similarly prepares linked members with internal single-signature evidence for their peers, keeping result bounds for recursive calls with an argument of Any. This evidence does not apply to ordinary host bindings or a local parameter that shadows a peer's name. It supplements closure discovery: literal bodies reached through contexts, list indexes, conditionals or iterators still specialize for the actual arguments. Complete peers also bound named calls by their written result projection or Null when the argument count permits binding, without claiming that the names match. Unknown and duplicate names stay runtime errors; excessive named arguments give only Null. The marker survives context-entry binding when a container exceeds the 32-entry shape limit. When body specialization reaches its depth or source budget, the analyzer uses the marked complete written signature even if closure discovery still succeeds. Discovery keeps its precedence while specialization is available. Descendant selection carries completeness from its source, including the functions inside the resulting lists. A source containing partial host functions or unknown values does not establish that evidence merely because the selected values have function types. Those calls keep conservative result bounds and checks.
Function literals distinguish their observable arrow from what their body returns on a call. For function(x: number) 1, the runtime result signature is nullable number, while the call refinement returns exactly 1. instance of reads the arrow, not that refinement. Lists and contexts preserve literal closures' call refinements; normalization does not widen their parameter domains. The signature comes from kind-only analysis of the written environment, before condition facts or call specialization; full analysis performs that pass before refining the body (D-219). Optimization preserves the captured signature too. For example, when v is declared Any, a function() v written under if v instance of number still has an observable Any result, even though a call in that branch can be known to return a number. This keeps instance of, callback acceptance and required result checks consistent across tiers and backends. Public projection keeps outer non-null parameters and falls back to Any if a safe domain cannot be written in the public type language. See the signature contract.
For guarded recursive data results, an enforced result declaration stays the exact runtime signature on both backends. Its observable analysis result is Any | Null; the structural projection is a call refinement, not proof of a structural signature. This also applies to functions declared in scopes and supplied as values. Recursive parameter domains use an empty lower bound in upper arrows. Functions inside recursive graph projections, overlapping runtime signatures and complete peers may keep broader call bounds. These approximations must keep reachable else branches and uncertain runtime checks.
Specialization also sees recursive parameters after conversion. It discards argument shape, callee and origin evidence that singleton conversion may invalidate, including in returned closures.
| Expression | Type | Warnings |
|---|---|---|
function(a: number) 1 | overload<function<number | Null> -> number | Null with function<number | Null> -> number = 1> | — |
(function(a: number) 1)(2) | number = 1 | — |
(function(a: number) 1) instance of function<number> -> number! | boolean | — |
null, and instance of
| Condition | f when true | f when not true |
|---|---|---|
f = null | Null | function<number | Null> -> number | Null |
f != null | function<number | Null> -> number | Null | Null |
f instance of function<number> -> number | function<number | Null> -> number | Null | Null |
f instance of function<string> -> number | overload<function<number | Null> -> number | Null with function<string | Null> -> number | Null> | function<number | Null> -> number | Null except function<string | Null> -> number | Null | Null |
f instance of function<Any> -> number | function<Any | Null> -> number | Null | function<number | Null> -> number | Null except function<Any | Null> -> number | Null | Null |
The last column of the last two rows is the function type followed by | Null: the alternative Null stays, and so does the function. The then of the second row is an overload: the function of a number, which behaves as the test says (gives a number or null) for a string.
f instance of Tremovesnullfromthenfor a function targetT, sincenullis an instance ofNullonly.- The meeting of two function types (
Feel.Type.Union.intersect/2,Feel.Type.Arrows.meet/2, D-171) is the one that is within the other (function<Any> -> numberis withinfunction<number> -> number: a function of anything is a function of a number, sothenhas the test's type), and if neither is, the function's own type together with the test's type, which describes what a call does for the arguments it takes. For the narrowing, and for a reader that does not call it, the result is the function's own type, which holds every function the name can be; a call finds the other (overloaded functions). - In
elsethe function is removed if its type is wholly withinT(Union.subtype?/2), as inf instance of function<number> -> number; if it is not, what is left is the function type lessT(function<...> -> ... except function<...> -> ..., D-152), a type that holds the functions of the name that are not instances ofT. - Parameters are contravariant and the result covariant in that test, as the conformance rules of the lattice have it (type-system.md, 2): a
function<Any> -> numberis wherever afunction<number> -> numberis expected.
What a function does not narrow
| Expression | Type | Warnings |
|---|---|---|
f(x) | number | Null | — |
if f != null then f(1) else 0 | number | Null | — |
f(x, y) | Any | Null | — |
Removing null from the callee does not remove null from its declared result: f(1) stays number | Null after f != null. An instance of fact can additionally refine what a call returns for arguments in the tested domain; it does not establish a function's implementation. A call with a different number of arguments is Any | Null: the type says that the function has this arrow, not that it has no other (ceiling is a function<number> -> number and takes a scale too, D-171); a function value known from its syntax or registry can establish a real arity error, but an overload type alone cannot. A named call through a type alone is Any | Null, since names can select an unrecorded signature. How a call of a value is typed, and the choice between several function types, are covered in overloaded functions.
Any and functions
A name of type Any is taken kind by kind by a comparison, and the function is the one kind that has no value set: where a function may be accepted, the then has function, a function of any parameters (:function, D-39, D-152):
| Condition | v when true | v when not true |
|---|---|---|
v instance of function<number> -> number | function<number | Null> -> number | Null | Any except function<number | Null> -> number | Null | Null |
v = f | function | Null | Any | Null |
v = f can be true when v is a function (two functions are equal when they are the same value), and the narrowing does not say which, so v is function; and null is included when f may be null. An ordering never accepts a function, so v > 5 leaves the numbers (Any). instance of is the one condition that takes functions out of Any in else: v instance of function<number> -> number leaves Any, since a function of other parameters may be there, and the other conditions of Any take the other kinds out and leave the function in. The allowed values of a name of type Any are read in the same way: = f leaves function | Null.
Limits
- No finite set of function values. Exclusions operate on signature descriptions (D-171), subject to bounded representation and projection. The signature/projection contracts are stated in the assessment and kept as regressions. These bounds do not provide a complete semantic decision procedure or establish equal behavior for two functions with matching metadata.
- A call by name does not look at the names of the parameters: the type has none, and it cannot be known which ones the call supplies.
- A function that is written in the expression and called there is analyzed again with the types of its arguments, so the facts that hold at the call narrow its body (where facts apply, D-193). The same holds for one that the callee node identifies: a key of a context literal, an item of a list literal, a branch of an
if(D-193, overloaded functions), and the call has the values that are also in what the type of the callee gives, which a condition narrows (f instance of function<number> -> numberbeforef(1)). The same holds for one that a name holds in a list or a context that is written out, one that a parameter is given as anif, an item or a key, and the iterator of aforover a list of them (D-193): the shapes of the structure of a name keep which functions its items and entries are. The same holds for a function that a call returns (g(1)(2), D-193), one that is read through a list or a context literal that is selected from more than once ({fs: [abs, floor]}.fs[1](x)), and one that a parameter is given in a list or a context. - A function that reaches the call through a call that returns a list or a context, through a call that may give a value that is not a function, or in more than 4 alternatives is typed by the type: the type of a function says its arrows and nothing of the body.
See also: overloaded functions, Any, null.