6. Functions
Functions, be they function declarations, expressions or even methods, are internally modeled by means of a function type. In this chapter, the general function type is described along with its semantics and type constraints. Function definitions and expressions are then introduced in terms of statements and expressions. Method definitions and special usages are described in Methods.
6.1. Function Type
A function type is modeled as Object
(see [ECMA11a(p.S13, p.p.98)] in ECMAScript.
Function types can be defined by means of;
-
A function object (Function-Object-Type).
-
A function type expression (Type Expressions).
-
A function declaration (Function Declaration).
-
A method declaration (Methods).
6.1.1. Properties
In any case, a function type declares the signature of a function and allows validation of calls to that function. A function type has the following properties:
typePars
-
(0-indexed) list of type parameters (i.e. type variables) for generic functions.
fpars
-
(0-indexed) list of formal parameters.
returnType
-
(possibly inferred) return type (expression) of the function or method.
name
-
Name of function or method, may be empty or automatically generated (for messages).
body
-
The body of the function, it contains statements . The body is null if a function type is defined in a type expression, and it is the last argument in case of a function object constructor, or the content of the function definition body.
Additionally, the following pseudo properties for functions are defined:
thisTypeRef
-
The this type ref is the type to which the
this
-keyword would be evaluated if used inside the function or member. The inference rules are described in This Keyword. fpars
-
List of formal parameters and the this type ref. This is only used for sub typing rules. If
this
is not used inside the function, thenany
is set instead of the inferred thisTypeRef to allow for more usages. The property is computed as follows:
Parameters (in ) have the following properties:
name
-
Name of the parameter.
type
-
Type (expression) of the parameter. Note that only parameter types can be variadic or optional.
The function definition can be annotated similar to Methods except that the final
and abstract
modifiers aren’t supported for function declarations.
A function declaration is always final and never abstract.
Also, a function has no property advice set.
Semantics
Req. IDE-79: Function Type (ver. 1)
Type Given a function type , the following constraints must be true:
-
Optional parameters must be defined at the end of the (formal) parameter list. In particular, an optional parameter must not be followed by a non-optional parameter:
-
Only the last parameter of a method may be defined as variadic parameter:
-
If a function explicitly defines a return type, the last statement of the transitive closure of statements of the body must be a return statement:
-
If a function explicitly defines a return type, all return statements must return a type conform to that type:
6.1.2. Type Inference
Definition: Function Type Conformance Non-Parameterized
For the given non-parameterized function types
with
and
with
and
,
we say conforms to ,
written as , if and only if:
-
-
if :
-
else ():
Function Variance Chart shows a simple example with the function type conformance relations.
{function()}
{function(A)}
{function(A, A)}
might be surprising for Java programmers. However, in JavaScript it is
possible to call a function with any number of arguments independently
from how many formal parameters the function defines.
If a function does not define a return type, any
is assumed if at least one
of the (indirectly) contained return statements contains an expression.
Otherwise void
is assumed. This is also true if there is an error due to
other constraint violations.
with
The following incomplete snippet demonstrates the usage of two function variables and , in which must hold true according to the aforementioned constraints.
A function bar
declares a parameter , which is actually a function itself.
is a variable, to which a function expression is a assigned.
Function bar
is then called with as an argument.
Thus, the type of must be a subtype of the ’s type.
function bar(f1: {function(A,B):C}) { ... }
var f2: {function(A,B):C} = function(p1,p2){...};
bar(f1);
The type of this
can be explicitly set via the @This
annotation.
function f(): A {..}
function p(): void {..}
fAny(log: {function():any}) {...}
fVoid(f: {function():void}) {..}
fA(g: {function():A}) {...}
fAny(f); // --> ok A <: any
fVoid(f); // -->error A !<: void
fA(f); // --> ok (easy) A <: A
fAny(p); // --> ok void <: any
fVoid(p); // --> ok void <: void
fA(p); // --> error void !<: A
If classes A, B, and C are defined as previously mentioned, i.e. , then the following subtyping relations with function types are to be evaluated as follows:
{function(B):B} <: {function(B):B} -> true
{function():A} <: {function():B} -> false
{function():C} <: {function():B} -> true
{function(A)} <: {function(B)} -> true
{function(C)} <: {function(B)} -> false
{function():void} <: {function():void} -> true
{function():undefined} <: {function():void} -> true
{function():void} <: {function():undefined} -> true (!)
{function():B} <: {function():void} -> true (!)
{function():B} <: {function():undefined} -> false (!)
{function():void} <: {function():B} -> false
{function():undefined} <: {function():B} -> true
The following examples demonstrate the effect of optional and variadic parameters:
{function(A)} <: {function(B)} -> true
{function(A...)} <: {function(A)} -> true
{function(A, A)} <: {function(A)} -> false
{function(A)} <: {function(A,A)} -> true (!)
{function(A, A...)} <: {function(A)} -> true
{function(A)} <: {function(A,A...)} -> true (!)
{function(A, A...)} <: {function(B)} -> true
{function(A?)} <: {function(A?)} -> true
{function(A...)} <: {function(A...)} -> true
{function(A?)} <: {function(A)} -> true
{function(A)} <: {function(A?)} -> false
{function(A...)} <: {function(A?)} -> true
{function(A?)} <: {function(A...)} -> true (!)
{function(A,A...)} <: {function(A...)} -> false
{function(A,A?)} <: {function(A...)} -> false
{function(A?,A...)} <: {function(A...)} -> true
{function(A...)} <: {function(A?,A...)} -> true
{function(A...)} <: {function(A?)} -> true
{function(A?,A?)} <: {function(A...)} -> true (!)
{function(A?,A?,A?)} <: {function(A...)} -> true (!)
{function(A?)} <: {function()} -> true (!)
{function(A...)} <: {function()} -> true (!)
The following examples demonstrate the effect of optional return types:
{function():void} <: {function():void} -> true
{function():X} <: {function():void} -> true
{function():X?} <: {function():void} -> true
{function():void} <: {function():Y} -> false
{function():X} <: {function():Y} -> X <: Y
{function():X?} <: {function():Y} -> false (!)
{function():void} <: {function():Y?} -> true (!)
{function():X} <: {function():Y?} -> X <: Y
{function():X?} <: {function():Y?} -> X <: Y
{function():B?} <: {function():undefined} -> false (!)
{function():undefined} <: {function():B?} -> true
The following examples show the effect of the @This
annotation:
{@This(A) function():void} <: {@This(X) function():void} -> false
{@This(B) function():void} <: {@This(A) function():void} -> false
{@This(A) function():void} <: {@This(B) function():void} -> true
{@This(any) function():void} <: {@This(X) function():void} -> true
{function():void} <: {@This(X) function():void} -> true
{@This(A) function():void} <: {@This(any) function():void} -> false
{@This(A) function():void} <: {function():void} -> false
Definition: Function Type Conformance
For the given function types
with
with
,
we say conforms to , written as , if and only if:
-
if :
-
else if
:-
(cf. Function Type Conformance Non-Parameterized )
(i.e. there exists a substitution of type variables in so that after substitution it becomes a subtype of as defined by Function Type Conformance Non-Parameterized)
-
-
else if :
-
( accordingly)
-
-
with and
(i.e. we replace each type variable in by the corresponding type variable at the same index in and check the constraints from Function Type Conformance Non-Parameterized as if and were non-parameterized functions and, in addition, the upper bounds on the left side need to be supertypes of the upper bounds on the right side).
-
Note that the upper bounds on the left must be supertypes of the right-side upper bounds (for similar reasons why types of formal parameters on the left are required to be supertypes of the formal parameters’ types in ). Where a particular type variable is used, on co- or contra-variant position, is not relevant:
class A {}
class B extends A {}
class X {
<T extends B> m(): T { return null; }
}
class Y extends X {
@Override
<T extends A> m(): T { return null; }
}
Method m
in Y
may return an A
, thus breaking the contract of m in X
, but only if it is parameterized to do so, which is not allowed for clients of X
, only those of Y
.
Therefore, the override in the above example is valid.
The subtype relation for function types is also applied for method overriding to ensure that an overriding method’s signature conforms to that of the overridden method,
see [Req-IDE-72] (applies to method comnsumption and implementation accordingly, see [Req-IDE-73] and [Req-IDE-74]).
Note that this is very different from Java which is far more restrictive when checking overriding methods.
As Java also supports method overloading: given two types with and a super class method void m(B param)
, it is valid to override m
as void m(A param)
in N4JS but not in Java.
In Java this would be handled as method overloading and therefore an @Override
annotation on m
would produce an error.
Req. IDE-80: Upper and Lower Bound of a Function Type (ver. 1)
The upper bound of a function type is a function type with the lower bound types of the parameters and the upper bound of the return type:
The lower bound of a function type is a function type with the upper bound types of the parameters and the lower bound of the return type:
6.1.3. Autoboxing of Function Type
Function types, compared to other types like String, come only in on flavour: the Function object representation. There is no primitive function type. Nevertheless, for function type expressions and function declarations, it is possible to call the properties of Function object directly. This is similar to autoboxing for strings.
// function declaration
var param: number = function(a,b){}.length // 2
function a(x: number) : number { return x*x; }
// function reference
a.length; // 1
// function variable
var f = function(m,l,b){/*...*/};
f.length; // 3
class A {
s: string;
sayS(): string{ return this.s; }
}
var objA: A = new A();
objA.s = "A";
var objB = {s:"B"}
// function variable
var m = objA.sayS; // method as function, detached from objA
var mA: {function(any)} = m.bind(objA); // bind to objA
var mB: {function(any)} = m.bind(objB); // bind to objB
m() // returns: undefined
mA() // returns: A
mB() // returns: B
m.call(objA,1,2,3); // returns: A
m.apply(objB,[1,2,3]); // returns: B
m.toString(); // returns: function sayS(){ return this.s; }
6.1.4. Arguments Object
A special arguments object is defined within the body of a function.
It is accessible through the implicitly-defined local variable named ,
unless it is shadowed by a local variable, a formal parameter or a
function named arguments
or in the rare case that the function itself is called ’arguments’ [ECMA11a(p.S10.5, p.p.59)].
The argument object has array-like behavior even though it is not of type array
:
-
All actual passed-in parameters of the current execution context can be retrieved by index access.
-
The
length
property of the arguments object stores the actual number of passed-in arguments which may differ from the number of formally defined number of parameters of the containing function. -
It is possible to store custom values in the arguments object, even outside the original index boundaries.
-
All obtained values from the arguments object are of type
any
.
In non-strict ES mode the callee
property holds a reference to the function executed [ECMA11a(p.S10.6, p.p.61)].
Req. IDE-81: Arguments.callee (ver. 1)
In N4JS and in ES strict mode the use of arguments.callee
is prohibited.
Req. IDE-82: Arguments as formal parameter name (ver. 1)
In N4JS, the formal parameters of the function cannot be named arguments
.
This applies to all variable execution environments like field accessors (getter/setter, Field Accessors (Getter/Setter)),
methods (Methods) and constructors (Constructor and Classifier Type), where FormalParameter
type is used.
// regular function
function a1(s1: string, n2: number) {
var l: number = arguments.length;
var s: string = arguments[0] as string;
}
class A {
// property access
get s(): string { return ""+arguments.length; } // 0
set s(n: number) { console.log( arguments.length ); } // 1
// method
m(arg: string) {
var l: number = arguments.length;
var s: string = arguments[0] as string;
}
}
// property access in object literals
var x = {
a:5,
get b(): string {
return ""+arguments.length
}
}
// invalid:
function z(){
arguments.length // illegal, see next lines
// define arguments to be a plain variable of type number:
var arguments: number = 4;
}
6.2. ECMAScript 5 Function Definition
6.2.1. Function Declaration
6.2.1.1. Syntax
A function can be defined as described in [ECMA11a(p.S13, p.p.98)] and additional annotations can be specified. Since N4JS is based on [ECMA15a], the syntax contains constructs not available in [ECMA11a]. The newer constructs defined only in [ECMA15a] and proposals already implemented in N4JS are described in ECMAScript 2015 Function Definition and ECMAScript Proposals Function Definition.
In contrast to plain JavaScript, function declarations can be used in blocks in N4JS. This is only true, however, for N4JS files, not for plain JS files. |
FunctionDeclaration <Yield>:
=> ({FunctionDeclaration}
annotations+=Annotation*
(declaredModifiers+=N4Modifier)*
-> FunctionImpl <Yield,Yield,Expression=false>
) => Semi?
;
fragment AsyncNoTrailingLineBreak *: (declaredAsync?='async' NoLineTerminator)?;
fragment FunctionImpl<Yield, YieldIfGenerator, Expression>*:
'function'
(
generator?='*' FunctionHeader<YieldIfGenerator,Generator=true> FunctionBody<Yield=true,Expression>
| FunctionHeader<Yield,Generator=false> FunctionBody<Yield=false,Expression>
)
;
fragment FunctionHeader<Yield, Generator>*:
TypeVariables?
name=BindingIdentifier<Yield>?
StrictFormalParameters<Yield=Generator>
(-> ':' returnTypeRef=TypeRef)?
;
fragment FunctionBody <Yield, Expression>*:
<Expression> body=Block<Yield>
| <!Expression> body=Block<Yield>?
;
Properties of the function declaration and expression are described in Function Type.
For this specification, we introduce a supertype for both, and . This supertype contains all common properties of these two subtypes, that is, all properties of .
// plain JS
function f(p) { return p.length }
// N4JS
function f(p: string): number { return p.length }
6.2.1.2. Semantics
A function defined in a class’s method (or method modifier) builder is a method, see Methods for details and additional constraints.
The metatype of a function definition is function type (Function Type), as a function declaration is only a different syntax for creating a Function
object.
Constraints for function type are described in Function Type.
Another consequence is that the inferred type of a function definition is simply its function type .
Note that the type of a function definition is different from its return type !
Req. IDE-83: Function Declaration only on Top-Level (ver. 1)
-
In plain JavaScript, function declarations must only be located on top-level, that is they must not be nested in blocks. Since this is supported by most JavaScript engines, only a warning is issued.
6.2.2. Function Expression
A function expression [ECMA11a(p.S11.2.5)] is quite similar to a function declaration. Thus, most details are explained in ECMAScript 5 Function Definition.
6.2.2.1. Syntax
FunctionExpression:
({FunctionExpression}
FunctionImpl<Yield=false,YieldIfGenerator=true,Expression=true>
)
;
6.2.2.2. Semantics and Type Inference
In general, the inferred type of a function expression simply is the function type as described in Function Type. Often, the signature of a function expression is not explicitly specified but it can be inferred from the context. The following context information is used to infer the full signature:
-
If the function expression is used on the right hand side of an assignment, the expected return type can be inferred from the left hand side.
-
If the function expression is used as an argument in a call to another function, the full signature can be inferred from the corresponding type of the formal parameter declaration.
Although the signature of the function expression may be inferred from the formal parameter if the function expression is used as argument, this inference has some conceptual limitations. This is demonstrated in the next example.
In general, {function():any}
is a subtype of {function():void}
(cf. Function Type).
When the return type of a function expression is inferred, this relation is taken into account which may lead to unexpected results as shown in the following code snippet:
function f(cb: {function():void}) { cb() }
f(function() { return 1; });
No error is issued: The type of the function expression actually is inferred to {function():any}
, because there is a return statement with an expression.
It is not inferred to {function():void}
, even if the formal parameter of f
suggests that.
Due to the previously-stated relation {function():any} <: {function():void}
this is correct – the client (in this
case function f
) works perfectly well even if cb
returns something.
The contract of arguments states that the type of the argument is a subtype of the type of the formal parameter.
This is what the inferencer takes into account!
6.3. ECMAScript 2015 Function Definition
6.3.1. Formal Parameters
Parameter handling has been significantly upgraded in ECMAScript 6. It now supports parameter default values, rest parameters (variadics) and destructuring. Formal parameters can be modified to be either default or variadic. In case a formal parameter has no modifier, it is called normal. Modified parameters also become optional.
Modifiers of formal parameters such as default or rest are neither evaluated nor rewritten in the transpiler.
6.3.1.1. Optional Parameters
An optional formal parameter can be omitted when calling a function/method.
An omitted parameter has the value undefined
.
In case the omitted parameter is variadic, the value is an empty array.
Parameters can not be declared as optional explicitly. Instead, being optional is true when a parameter is declared as default or variadic. Note that any formal parameter that follows a default parameter is itself also a default thus an optional parameter.
6.3.1.2. Default Parameters
A default parameter value is specified for a parameter via an equals sign (=
).
If a caller doesn’t provide a value for the parameter, the default value is used.
Default initializers of parameters are specified at a formal parameter of a function or method after the equal sign using an arbitrary initializer expression, such as var = "s"
.
However, this default initializer can be omitted.
When a formal parameter has a declared type, the default initializer is specified at the end, such as: var : string = "s"
.
The initializer expression is only evaluated in case no actual argument is given for the formal parameter.
Also, the initializer expression is evaluated when the actual argument value is undefined
.
Formal parameters become default parameters implicitly when they are preceded by an explicit default parameter.
In such cases, the default initializer is undefined
.
Req. IDE-14501: Default parameters (ver. 1)
Any normal parameter which is preceded by a default parameter also becomes a default parameter.
Its initializer is undefined
.
When a method is overwritten, its default parameters are not part of the overwriting method. Consequently, initializers of default parameters in abstract methods are obsolete.
6.3.1.3. Variadic
Variadic parameters are also called rest parameters. Marking a parameter as variadic indicates that method accepts a variable number of parameters. A variadic parameter implies that the parameter is also optional as the cardinality is defined as . No further parameter can be defined after a variadic parameter. When no argument is given for a variadic parameter, an empty array is provided when using the parameter in the body of the function or method.
Req. IDE-16: Variadic and optional parameters (ver. 1)
For a parameter , the following condition must hold: .
A parameter can not be declared both variadic and with a default value. That is to say that one can either write (default) or , but not .
Declaring a variadic parameter of type causes the type of the method parameter to become Array<T>
.
That is, declaring function(…tags : string)
causes tags
to be an Array<string>
and not just a scalar string
value.
To make this work at runtime, the compiler will generate code that constructs the parameter
from the arguments
parameter explicitly passed to the function.
Req. IDE-17: Variadic at Runtime (ver. 1)
At runtime, a variadic parameter is never set to undefined. Instead, the array may be empty. This must be true even if preceding parameters are optional and no arguments are passed at runtime.
For more constraints on using the variadic modifier, see Function-Object-Type.
6.3.2. Generator Functions
Generators come together with the yield
expression and can play three roles:
the role of an iterator (data producer), of an observer (data consumer), and a combined role which is called coroutines.
When calling a generator function or method, the returned generator object of type Generator<TYield,TReturn,TNext>
can be controlled by its methods
(cf. [ECMA15a(p.S14.4)], also see [Kuizinas14a]).
6.3.2.1. Syntax
Generator functions and methods differ from ordinary functions and methods only in the additional *
symbol before the function or method name.
The following syntax rules are extracted from the real syntax rules.
They only display parts relevant to declaring a function or method as a generator.
GeneratorFunctionDeclaration <Yield>:
(declaredModifiers+=N4Modifier)*
'function' generator?='*'
FunctionHeader<YieldIfGenerator,Generator=true>
FunctionBody<Yield=true,Expression=false>
;
GeneratorFunctionExpression:
'function' generator?='*'
FunctionHeader<YieldIfGenerator,Generator=true>
FunctionBody<Yield=true,Expression=true>
;
GeneratorMethodDeclaration:
annotations+=Annotation+ (declaredModifiers+=N4Modifier)* TypeVariables?
generator?='*' NoLineTerminator LiteralOrComputedPropertyName<Yield>
MethodParamsReturnAndBody<Generator=true>
6.3.2.2. Semantics
The basic idea is to make code dealing with Generators easier to write and more readable without changing their functionality. Take this example:
// explicit form of the return type
function * countTo(iMax:int) : Generator<int,string,undefined> {
for (int i=0; i<=iMax; i++)
yield i;
return "finished";
}
val genObj1 = countTo(3);
val values1 = [...genObj1]; // is [0,1,2,3]
val lastObj1 = genObj1.next(); // is {value="finished",done=true}
// shorthand form of the return type
function * countFrom(start:int) : int {
for (int i=start; i>=0; i--)
yield i;
return finished;
}
val genObj2 = countFrom(3);
val values2 = [...genObj2]; // is [3,2,1,0]
val lastObj2 = genObj2.next(); // is {value="finished",done=true}
In the example above, two generator functions are declared. The first declares its return type explicitly whereas the second uses a shorthand form.
Generator functions and methods return objects of the type Generator<TYield,TReturn,TNext>
which is a subtype of the Iterable<TYield>
and Iterator<TYield>
interfaces.
Moreover, it provides the methods throw(exception:any)
and return(value:TNext?)
for advanced control of the generator object.
The complete interface of the generator class is given below.
public providedByRuntime interface Generator<out TYield, out TReturn, in TNext>
extends Iterable<TYield>, Iterator<TYield> {
public abstract next(value: TNext?): IteratorEntry<TYield>
public abstract [Symbol.iterator](): Generator<TYield, TReturn, TNext>
public abstract throw(exception: any): IteratorEntry<TYield>;
public abstract return(value: TNext?): IteratorEntry<TReturn>;
}
Req. IDE-14370: Modifier *
(ver. 1)
-
*
may be used on declared functions and methods, and for function expressions. -
A function or method f with a declared return type R that is declared
*
has an actual return type ofGenerator<TYield,TReturn,TNext>
. -
A generator function or method can have no declared return type, a shorthand form of a return type or an explicitly declared return type.
-
The explicitly declared return type is of the form
Generator<TYield,TReturn,TNext>
with the type variables:-
TYield as the expected type of the yield expression argument,
-
TReturn as the expected type of the return expression, and
-
TNext as both the return type of the yield expression.
-
-
The shorthand form only declares the type of TYield which implicitly translates to
Generator<TYield,TReturn,any>
as the return type.-
The type TReturn is inferred to either
undefined
orany
from the body. -
In case the declared type is
void
, actual return type evaluates toGenerator<undefined,undefined,any>
.
-
-
If no return type is declared, both TYield and TReturn are inferred from the body to either
any
orundefined
. TNext isany
.
-
-
Given a generator function or method f with an actual return type
Generator<TYield,TReturn,TNext>
:-
all yield statements in f must have an expression of type TYield.
-
all return statements in f must have an expression of type TReturn.
-
-
Return statements in generator functions or methods are always optional.
Req. IDE-14371: Modifier yield
and yield*
(ver. 1)
-
yield
andyield*
may only be in body of generator functions or methods. -
yield expr
takes only expressions expr of type TYield in a generator function or methods with the actual typeGenerator<TYield,TReturn,TNext>
. -
The return type of the
yield
expression is TNext. -
yield* fg()
takes only iterators of typeIterator<TYield>
, and generator functions or methods fg with the actual return typeGenerator<? extends TYield,? extends TReturn,? super TNext>
. -
The return type of the
yield*
expression is any, since a custom iterator could return an entry{done=true,value}
and any value for the variablevalue
.
Similar to async
functions, shorthand and explicit form * function():int{};
and * function():Generator<int,TResult,any>
are equal,
given that the inferred TResult of the former functions equals to TResult in the latter function).
In other words, the return type of generator functions or methods is wrapped when it is not explicitly defined as Generator
already.
Thus, whenever a nested generator type is desired, it has to be defined explicitly.
Consider the example below.
class C<T> {
genFoo(): T{} // equals to genFoo(): Generator<T, undefined, any>;
// note that TResult depends on the body of genFoo()
}
function fn(C<int> c1, C<Generator<int,any,any>> c2) {
c1.genFoo(); // returns Generator<int, undefined, any>
c2.genFoo(); // returns Generator<Generator<int,any,any>, undefined, any>
}
6.3.2.3. Generator Arrow Functions
As of now, generator arrow functions are not supported by EcmaScript 6 and also, the support is not planned. However, introducing generator arrow function in EcmaScript is still under discussion. For more information, please refer to ESDiscuss.org and StackOverflow.com.
6.3.3. Arrow Function Expression
This is an ECMAScript 6 expression (see [ECMA15a(p.S14.2)]) for simplifying the definition of anonymous function expressions, a.k.a. lambdas or closures. The ECMAScript Specification calls this a function definition even though they may only appear in the context of expressions.
Along with Assignments, Arrow function expressions have the least precedence, e.g. they serve as the entry point for the expression tree.
Arrow function expressions can be considered syntactic window-dressing for old-school function expressions and therefore do not support the
benefits regarding parameter annotations although parameter types may be given explicitly.
The return type can be given as type hint if desired, but this is not mandatory (if left out, the return type is inferred).
The notation @=>
stands for an async arrow function (Asynchronous Arrow Functions).
6.3.3.1. Syntax
The simplified syntax reads like this:
ArrowExpression returns ArrowFunction:
=>(
{ArrowFunction}
(
'('
( fpars+=FormalParameterNoAnnotations ( ',' fpars+=FormalParameterNoAnnotations )* )?
')'
(':' returnTypeRef=TypeRef)?
| fpars+=FormalParameterNoType
)
'=>'
) (
(=> hasBracesAroundBody?='{' body=BlockMinusBraces '}') | body=ExpressionDisguisedAsBlock
)
;
FormalParameterNoAnnotations returns FormalParameter:
(declaredTypeRef=TypeRef variadic?='...'?)? name=JSIdentifier
;
FormalParameterNoType returns FormalParameter: name=JSIdentifier;
BlockMinusBraces returns Block: {Block} statements+=Statement*;
ExpressionDisguisedAsBlock returns Block:
{Block} statements+=AssignmentExpressionStatement
;
AssignmentExpressionStatement returns ExpressionStatement: expression=AssignmentExpression;
6.3.3.2. Semantics and Type Inference
Generally speaking, the semantics are very similar to the function expressions but the devil’s in the details:
-
arguments
: Unlike normal function expressions, an arrow function does not introduce an implicitarguments
variable (Arguments Object), therefore any occurrence of it in the arrow function’s body has always the same binding as an occurrence ofarguments
in the lexical context enclosing the arrow function. -
this
: An arrow function does not introduce a binding of its own for thethis
keyword. That explains why uses in the body of arrow function have the same meaning as occurrences in the enclosing lexical scope. As a consequence, an arrow function at the top level has both usages ofarguments
andthis
flagged as error (the outer lexical context doesn’t provide definitionsfor them). -
super
: As with function expressions in general, whether of the arrow variety or not, the usage ofsuper
isn’t allowed in the body of arrow functions.
Req. IDE-84: No This in Top Level Arrow Function in N4JS Mode (ver. 1)
In N4JS, a top-level arrow function can’t refer to this
as there’s no outer lexical context that provides a binding for it.
Req. IDE-85: No Arguments in Top Level Arrow Function (ver. 1)
In N4JS, a top-level arrow function can’t include usages of arguments
in its body, again because of the missing binding for it.
6.4. ECMAScript Proposals Function Definition
6.4.1. Asynchronous Functions
To improve language-level support for asynchronous code, there exists an ECMAScript proposal [45] based on Promises which are provided by ES6 as built-in types. N4JS implements this proposal. This concept is supported for declared functions and methods (Asynchronous Methods) as well as for function expressions and arrow functions (Asynchronous Arrow Functions).
6.4.1.1. Syntax
The following syntax rules are extracted from the real syntax rules. They only display parts relevant to declaring a function or method as asynchronous.
AsyncFunctionDeclaration <Yield>:
(declaredModifiers+=N4Modifier)*
declaredAsync?='async' NoLineTerminator 'function'
FunctionHeader<Yield,Generator=false>
FunctionBody<Yield=false,Expression=false>
;
AsyncFunctionExpression:
declaredAsync?='async' NoLineTerminator 'function'
FunctionHeader<Yield=false,Generator=false>
FunctionBody<Yield=false,Expression=true>
;
AsyncArrowExpression <In, Yield>:
declaredAsync?='async' NoLineTerminator '('
(fpars+=FormalParameter<Yield>
(',' fpars+=FormalParameter<Yield>)*)?
')' (':' returnTypeRef=TypeRef)? '=>'
( '{' body=BlockMinusBraces<Yield> '}'
| body=ExpressionDisguisedAsBlock<In>
)
;
AsyncMethodDeclaration:
annotations+=Annotation+ (declaredModifiers+=N4Modifier)* TypeVariables?
declaredAsync?='async' NoLineTerminator LiteralOrComputedPropertyName<Yield>
MethodParamsReturnAndBody
’async’ is not a reserved word in ECMAScript and it can therefore be
used either as an identifier or as a keyword, depending on the context.
When used as a modifier to declare a function as asynchronous, then
there must be no line terminator after the async
modifier. This enables the
parser to distinguish between using async
as an identifier reference and a
keyword, as shown in the next example.
async (1)
function foo() {}
// vs
async function bar(); (2)
1 | In this snippet, the async on line 1 is an identifier reference (referencing a
variable or parameter) and the function defined on line 2 is a
non-asynchronous function. The automatic semicolon insertion adds a
semicolon after the reference on line 1. |
2 | In contrast, async on line 4 is recognized as a modifier declaring the function as asynchronous. |
6.4.1.2. Semantics
The basic idea is to make code dealing with Promises easier to write and more readable without changing the functionality of Promises. Take this example:
// some asynchronous legacy API using promises
interface DB {}
interface DBAccess {
getDataBase(): Promise<DB,?>
loadEntry(db: DB, id: string): Promise<string,?>
}
var access: DBAccess;
// our own function using async/await
async function loadAddress(id: string) : string {
try {
var db: DB = await access.getDataBase();
var entry: string = await access.loadEntry(db, id);
return entry.address;
}
catch(err) {
// either getDataBase() or loadEntry() failed
throw err;
}
}
The modifier async
changes the return type of loadAddress()
from string
(the declared return type) to Promise<string,?>
(the actual return type).
For code inside the function, the return type is still string
:
the value in the return statement of the last line will be wrapped in a Promise.
For client code outside the function and in case of recursive invocations, the return type is Promise<string,?>
.
To raise an error, simply throw an exception, its value will become the error value of the returned Promise.
If the expression after an await
evaluates to a Promise
, execution of the enclosing asynchronous function will be suspended until either a success value is available
(which will then make the entire await-expression evaluate to this success value and continue execution)
or until the Promise is rejected (which will then cause an exception to be thrown at the location of the await-expression).
If, on the other hand, the expression after an await
evaluates to a non-promise, the value will be simply passed through.
In addition, a warning is shown to indicate the unnecessary await
expression.
Note how method loadAddress()
above can be implemented without any explicit references to the built-in type Promise.
In the above example we handle the errors of the nested asynchronous calls to getDataBase()
and loadEntry()
for demonstration purposes only;
if we are not interested in the errors we could simply remove the try/catch block and any errors would be forwarded to the caller of loadAddress()
.
Invoking an async function commonly adopts one of two forms:
-
var p: Promise<successType,?> = asyncFn()
-
await asyncFn()
These patterns are so common that a warning is available whenever both
-
Promise
is omitted as expected type; and -
await
is also omitted.
The warning aims at hinting about forgetting to wait for the result, while remaining non-noisy.
Req. IDE-86: Modifier async
and await
(ver. 1)
-
async
may be used on declared functions and methods as well as for function expressions and arrow functions. -
A function or method that is declared
async
can have no declared return type, a shorthand form of a return type or an explicitly declared return type.-
The explicitly declared return type is of the form
Promise<R,E>
where R is the type of all return statements in the body, and E is the type of exceptions that are thrown in the body. -
The shorthand form only declares the type of R which implicitly translates to
Promise<R,?>
as the actual return type. -
In case no return type is declared, the type R of
Promise<R,?>
is inferred from the body.
-
-
A function or method f with a declared return type R that is declared
async
has an actual return type of-
R
if R is a subtype ofPromise<?,?>
, -
Promise<undefined,?>
if R is typevoid
. -
Promise<R,?>
in all other cases (i.e. the declared return type R is being wrapped in aPromise
).
-
-
Return type inference is only performed when no return type is declared.
-
The return type
R
ofPromise<R,?>
is inferred either asvoid
or asany
.
-
-
Given a function or method f that is declared
async
with a declared return type R, or with a declared return typePromise<R,?>
, all return statements in f must have an expression of type R (and not of typePromise<R,?>
). -
await
can be used in expressions directly enclosed in an async function, and behaves like a unary operator with the same precedence asyield
in ES6. -
Given an expression expr of type T, the type of (
await
expr) is inferred to T if T is not a Promise, or it is inferred to S if T is a Promise with a success value of type S, i.e. T <: Promise<S,?> .
In other words, the return type R of async
functions and methods will always be wrapped to Promise<R,?>
unless R is a Promise
already.
As a consequence, nested Promise
s as a return type of a async function or method have to be stated explicitly like Promise<Promise<R,?>,?>
.
When a type variable T
is used to define the the return type of an async function or method, it will always be wrapped.
Consider the example below.
interface I<T> {
async foo(): T; // amounts to foo(): Promise<T,?>
}
function snafu(i1: I<int>, i2: I<Promise<int,?>>) {
i1.foo(); // returns Promise<int,?>
i2.foo(); // returns Promise<Promise<int,?>,?>
}
6.4.1.3. Asynchronous Arrow Functions
An await
expression is allowed in the body of an async arrow function but not
in the body of a non-async arrow function. The semantics here are
intentional and are in line with similar constraint for function
expressions.
6.5. N4JS Extended Function Definition
6.5.1. Generic Functions
A generic function is a function with a list of generic type parameters. These type parameters can be used in the function signature to declare the types of formal parameters and the return type. In addition, the type parameters can be used in the function body, for example when declaring the type of a local variable.
In the following listing, a generic function foo
is defined that has two type parameters S
and T
.
Thereby S
is used as to declare the parameter type Array<S>
and T
is used as the return type and to construct the returned value in the function body.
function <S,T> foo(s: Array<S>): T { return new T(s); }
If a generic type parameter is not used as a formal parameter type or the return type, a warning is generated.
6.5.2. Promisifiable Functions
In many existing libraries, which have been developed in pre-ES6-promise-API times, callback methods are used for asynchronous behavior. An asynchronous function follows the following conventions:
'function' name '(' arbitraryParameters ',' callbackFunction ')'
Usually the function returns nothing (void
).
The callback function usually takes two arguments,in which the first is an error object and the other is the result value of the asynchronous operation.
The callback function is called from the asynchronous function, leading to nested function calls (aka ’callback hell’).
In order to simplify usage of this pattern, it is possible to mark such a function or method as @Promisifiable
.
It is then possible to ’promisify’ an invocation of this function or method, which means no callback function argument has to be provided and a will be returned.
The function or method can then be used as if it were declared with async
.
This is particularly useful in N4JS definition files (.n4jsd) to allow using an existing callback-based API from N4JS code with the more convenient await
.
Given a function with an N4JS signature
f(x: int, cb: {function(Error, string)}): void
This method can be annotated with Promisifiable
as follows:
@Promisifiable f(x: int, cb: {function(Error, string)}): void
With this annotation, the function can be invoked in four different ways:
f(42, function(err, result1) { /* ... */ }); // traditional
var promise: Promise<string,Error> = @Promisify f(42); // promise
var result3: string = await @Promisify f(42); // long
var result4: string = await f(42); // short
The first line is only provided for completeness and shows that a promisifiable function can still be used in the ordinary way by providing a callback - no special handling will occur in this case.
The second line shows how f
can be promisified using the @Promisify
annotation - no callback needs to be provided and instead, a Promise
will be returned.
We can either use this promise directly or immediately await
on it, as shown in line 3.
The syntax shown in line 4 is merely shorthand for await @Promisify
, i.e. the annotation is optional after await
.
Req. IDE-87: Promisifiable (ver. 1)
A function or method can be annotated with @Promisifiable
if and only if the following constraints hold:
-
Last parameter of is a function (the ).
-
The has a signature of
-
{function(E, T0, T1, …, Tn): V}
, or -
{function(T0, T1, …, Tn): V}
in which is type
Error
or a subtype thereof, are arbitrary types except or its subtypes. , if given, is then the type of the error value, and are the types of the success values of the asynchronous operation.
Since the return value of the synchronous function call is not available when using@Promisify
, is recommended to bevoid
, but it can be any type.
-
-
The callback parameter may be optional.[46]
According to [Req-IDE-87], a promisifiable function or method may or may not have a non-void return type, and that only the first parameter of the callback is allowed to be of type Error
, all other parameters must be of other types.
Req. IDE-88: @Promisify and await with promisifiable functions (ver. 1)
A promisifiable function with one of the two valid
signatures given in [Req-IDE-87] can be promisified with Promisify
or
used with await
, if and only if the following constraints hold:
-
Function must be annotated with
@Promisifiable
. -
Using
@Promisify f()
withoutawait
returns a promise of typePromise<S,F>
where-
is
IterableN<T0,…,Tn>
if ,T
if , andundefined
if . -
is
E
if given,undefined
otherwise.
-
-
Using
await @Promisify f()
returns a value of typeIterableN<T0,…,Tn>
if , of typeT
if , and of typeundefined
if . -
In case of using an
await
, the annotation can be omitted.
I.e.,await @Promisify f()
is equivalent toawait f()
. -
Only call expressions using f as target can be promisified, in other words this is illegal:
var pf = @Promisify f; // illegal code!