chharvey / counterpoint

A robust programming language.
GNU Affero General Public License v3.0
2 stars 0 forks source link

Syntax: Function Definitions, Void Functions #46

Open chharvey opened 3 years ago

chharvey commented 3 years ago

Define synchronous function declarations and function expressions.

Discussion

Function Definitions

This issue covers defining void synchronous functions only, which do return but do not return a value (#70). This issue does not cover non-void functions, asynchronous functions, or error throwing.

This is a function declaration:

func add(a: int, b: int): void {
    """
        The first argument is {{ a }}.
        The second argument is {{ b }}.
    """;
    "This function does not return a value.";
}

The name of this function is add. It has two input parameters a and b, each of type int. When a function is called (see v0.7.0), the values sent into the function are arguments. This function’s return type is void, because it has no output value. Its body is the set of statements within the curly braces.

This is a function expression (lambda):

(a: int, b: int): void {
    """
        The first argument is {{ a }}.
        The second argument is {{ b }}.
    """;
    "This function does not return a value.";
};

Lambdas are normal expressions that can be operated on and passed around like any other value. For instance, lambdas can be assigned to variables. Lambdas are always “truthy”.

let my_fn: (a: int, b: int) => void =
    (a: int, b: int): void { a + b; };
!!my_fn; %== true

Some parameters may be declared unfixed, which means they can be reassigned within the function body.

func add(unfixed a: int, b: int): void {
    a = a + 1; % ok
    b = b - 1; %> AssignmentError
}

Type Signatures

A function’s type signature is its type, written in the form of (‹params›) => ‹return›. It indicates what types of arguments are accepted and what type the function returns. This issue only covers functions that return void.

let my_fn: (a: int, b: int) => void =
    (a: int, b: int): void { a + b; };

% typeof my_fn: (a: int, b: int) => void

Function Assignment

When assigning a function to a type signature with named parameters (in the case of type alias assignment or abstract method implementation), the assigned parameter order must match up with the assignee parameters.

type BinaryOperatorType = (first: int, second: float) => void;
let add: BinaryOperatorType = (second: float, first: int): void { first + second; }; %> TypeError

TypeError: Type (second: float, first: int) => void is not assignable to type (first: int, second: float) => void.

The reason for this error is that one should expect to be able to call any BinaryOperatorType with the positional arguments of an int followed by a float. Calling it with e.g. 4.0 and 2, in that order, should fail. From this perspective, function assignment is a bit like tuple assignment.

From another perspective, function assignment is like record assignment: the parameter names of the assigned must match the parameter names of the assignee.

type BinaryOperatorType = (first: float, second: float) => void;
let subtract: BinaryOperatorType = (x: float, y: float): void { x - y; }; %> TypeError

TypeError: Type (x: float, y: float) => void is not assignable to type (first: float, second: float) => void.

This errors because a caller must be able to call subtract with the named arguments first and second.

Luckily, function parameter syntax has a built-in mechanism for handling function assignment/implementation with named parameters. In the parameter name, use first= x to alias the real parameter x to the assignee parameter first.

let subtract: BinaryOperatorType = (first= x: float, second= y: float): void { x - y; };

This lets the function author internally use the parameter names x and y while still allowing the caller to call the function with named arguments first and second repectively.

Variance

Function parameter types are contravariant. This means that when assigning a function g to a function type F, the type of each parameter of F must be assignable to the corresponding parameter’s type of g.

type UnaryOperator = (float | str) => void;
let g: UnaryOperator = (x: float): void { %> TypeError
    x; %: float
};

A type error is raised because we cannot assign a (float) => void type to a (float | str) => void type. Even though the parameter’s type is narrower, a caller should expect to be able to call any UnaryOperator implementation with a str argument, and our implementation doesn’t allow that.

However, we can widen the parameter types:

let h: UnaryOperator = (x: int | float | str): void {
    x; %: int | float | str
};

This meets the requirements of UnaryOperator but still has a wider type for its parameter.

Specification

Lexicon

Punctuator :::=
    // storage
+       | "=>"
;

Keyword :::=
    // storage
+       | "func"
;

Syntax

+TypeFunction<Named>
+   ::= "(" ","? ParameterType<?Named># ","? ")" "=>" "void";

Type ::=
    | TypeUnion
+   | TypeFunction<-Named, +Named>
;

+ExpressionFunction
+   ::= "(" ","? ParameterFunction# ","? ")" ":" "void" StatementBlock<-Break>;

Expression<Dynamic> ::=
    | ExpressionDisjunctive<?Dynamic>
    | ExpressionConditional<?Dynamic>
+   | <Dynamic+>ExpressionFunction
;

+ParameterType<Named>
+   ::= <Named+>(IDENTIFIER ":") Type;

+ParameterFunction
+   ::= (IDENTIFIER "=")? "unfixed"? IDENTIFIER ":" Type;

+DeclarationFunction
+   ::= "func" IDENTIFIER ExpressionFunction;

Declaration ::=
    | DeclarationVariable
    | DeclarationType
+   | DeclarationFunction
;

Semantics

SemanticType =:=
    | SemanticTypeConstant
    | SemanticTypeAlias
    | SemanticTypeEmptyCollection
    | SemanticTypeList
    | SemanticTypeRecord
    | SemanticTypeOperation
+   | SemanticTypeFunction
;

+SemanticTypeFunction
+   ::= SemanticParameterType*;

SemanticExpression =:=
    | SemanticConstant
    | SemanticVariable
    | SemanticTemplate
    | SemanticEmptyCollection
    | SemanticList
    | SemanticRecord
    | SemanticMapping
    | SemanticOperation
+   | SemanticFunction
;

+SemanticFunction
+   ::= SemanticParameter* SemanticBlock;

+SemanticParameterType
+   ::= SemanticVariable? SemanticType;

+SemanticParameter[unfixed: Boolean]
+   ::= SemanticKey? SemanticVariable SemanticType;

SemanticDeclaration =:=
    | SemanticDeclarationType
    | SemanticDeclarationVariable
+   | SemanticDeclarationFunction
;

+SemanticDeclarationFunction
+   ::= SemanticVariable SemanticTypeFunction SemanticFunction;

SemanticBlock
    ::= SemanticStatement*;

Decorate

+Decorate(TypeFunction ::= "(" ","? ParameterType# ","? ")" "=>" "void") -> SemanticTypeFunction
+   := (SemanticTypeFunction ...ParseList(ParameterType, SemanticParameterType));

+Decorate(Type ::= TypeFunction) -> SemanticTypeFunction
+   := Decorate(TypeFunction);

+Decorate(ExpressionFunction ::= "(" ","? ParameterFunction# ","? ")" ":" "void" StatementBlock<-Break>) -> SemanticFunction
+   := (SemanticFunction
+       ...ParseList(ParameterFunction, SemanticParameter)
+       Decorate(StatementBlock<-Break>)
+   );

+Decorate(Expression_Dynamic ::= ExpressionFunction) -> SemanticFunction
+   := Decorate(ExpressionFunction);

+Decorate(ParameterType ::= Type) -> SemanticParameterType
+   := (SemanticParameterType Decorate(Type));
+Decorate(ParameterType_Named ::= IDENTIFIER ":" Type) -> SemanticParameterType
+   := (SemanticParameterType
+       (SemanticVariable[id=TokenWorth(IDENTIFIER)])
+       Decorate(Type)
+   );

+Decorate(ParameterFunction ::= IDENTIFIER ":" Type) -> SemanticParameter
+   := (SemanticParameter[unfixed=false]
+       (SemanticVariable[id=TokenWorth(IDENTIFIER)])
+       Decorate(Type)
+   );
+Decorate(ParameterFunction ::= "unfixed" IDENTIFIER ":" Type) -> SemanticParameter
+   := (SemanticParameter[unfixed=true]
+       (SemanticVariable[id=TokenWorth(IDENTIFIER)])
+       Decorate(Type)
+   );
+Decorate(ParameterFunction ::= IDENTIFIER__0 "=" IDENTIFIER__1 ":" Type) -> SemanticParameter
+   := (SemanticParameter[unfixed=false]
+       (SemanticKey[id=TokenWorth(IDENTIFIER__0)])
+       (SemanticVariable[id=TokenWorth(IDENTIFIER__1)])
+       Decorate(Type)
+   );
+Decorate(ParameterFunction ::= IDENTIFIER__0 "=" "unfixed" IDENTIFIER__1 ":" Type) -> SemanticParameter
+   := (SemanticParameter[unfixed=true]
+       (SemanticKey[id=TokenWorth(IDENTIFIER__0)])
+       (SemanticVariable[id=TokenWorth(IDENTIFIER__1)])
+       Decorate(Type)
+   );

+Decorate(DeclarationFunction ::= "func" IDENTIFIER ExpressionFunction) -> SemanticDeclarationFunction
+   := (SemanticDeclarationFunction
+       (SemanticVariable[id=TokenWorth(IDENTIFIER)])
+       FunctionTypeOf(ExpressionFunction)
+       Decorate(ExpressionFunction)
+   );

+Decorate(Declaration ::= DeclarationFunction) -> SemanticDeclarationFunction
+   := Decorate(DeclarationFunction);

FunctionTypeOf

+FunctionTypeOf(ExpressionFunction ::= "(" ","? ParameterFunction# ","? ")" ":" "void" StatementBlock<-Break>) -> SemanticTypeFunction
+   := (SemanticTypeFunction ...FunctionTypeOf(ParameterFunction#));

+   FunctionTypeOf(ParameterFunction# ::= ParameterFunction) -> Tuple<SemanticParameterType>
+       := [FunctionTypeOf(ParameterFunction)];
+   FunctionTypeOf(ParameterFunction# ::= ParameterFunction# "," ParameterFunction) -> Sequence<SemanticParameterType>
+       := [
+           ...FunctionTypeOf(ParameterFunction#),
+           FunctionTypeOf(ParameterFunction),
+       ];

+FunctionTypeOf(ParameterFunction ::= "unfixed"? IDENTIFIER ":" Type) -> SemanticParameterType
+   := (SemanticParameterType
+       (SemanticVariable[id=TokenWorth(IDENTIFIER)])
+       Decorate(Type)
+   );
+FunctionTypeOf(ParameterFunction ::= IDENTIFIER__0 "=" "unfixed"? IDENTIFIER__1 ":" Type) -> SemanticParameterType
+   := (SemanticParameterType
+       (SemanticVariable[id=TokenWorth(IDENTIFIER__0)])
+       Decorate(Type)
+   );
chharvey commented 2 months ago

Semantics Discussion

Keyword void Must Be Isolated

Continued from #96. The keyword void is not a type and cannot be treated as types in the type system. It may only be used in return signatures of functions that execute completely but that do not return a value. It may not be combined with types. Functions can only return a type, or return void, but not a combination.

function return_void(): void { % ok
    print.("hello");
    return;
}

function return_int_or_void(): int | void %> SyntaxError
    => 42;

type VoidFn = () => void; % ok
type VoidOrIntFn = () => int | void; %> SyntaxError

class abstract Foo {
    public doSomething(): void; % ok

    public callVoidFn(void_fn: () => void): void { % ok
        return void_fn.();
    }
}

Void Function Returns

Void function bodies are now required to have an explicit return; statement in every code path. This cannot be enforced syntactically, because it requires control flow analysis.


function maybe_return_void(): void {
    print.("hello");
    return; % <- required statement
}

function return_void(): void {
    print.("hello");
    return; % <- required statement
}
function conditionally_return_void(b: bool): void {
    if b then {
        print.(b);
        return; % <- required statement
    } else {
        print.("""not {{ !b }}""");
        return; % <- required statement
    };
}
function no_else_branch(b: bool): void {
    if b then {
        print.(b);
    };
    return; % <- also valid
}
function invalid_no_else_branch(b: bool): void {
    if b then {
        print.(b);
        return;
    };
    %> SemanticError: Expected a `return` statment in all code paths.
}

Void Function Calls

Disallow assignment of void function calls in most cases.

function return_void(): void {
    print.("hello");
    return;
}
let val: unknown = return_void.(); %> AssignmentError

function take_unknown(arg: unknown): 0 {
    arg;
    return 0;
}
take_unknown.(return_void.()); %> AssignmentError

function return_unknown(): unknown {
    return return_void.(); %> AssignmentError
}

let tup:     [unknown]    = [return_void.()]; %> AssignmentError
let tup_opt: [?: unknown] = [return_void.()]; %> AssignmentError

There is one exception: A void function call may be returned from another void function. This allows us to utilize tail calls.

function print_hello(): void {
    return print.("hello"); % tail call allowed here
}

function print_world(): void
    => print.("world"); % implicit returns also allowed

The code above is valid, but only because the return signatures of the calling function and the called function are both void. If either function had a different return signature, we could not use the tail call.

With tail call optimization, the stack frame of the calling function may be reused to execute the called function. If returning void function calls were not allowed, we could not utilize this optimization.

function countdown(n: int): void {
    if n > 0 then {
        print.(n);
        countdown.(n - 1);
        return;
    } else {
        print.("Blast off!");
        return;
    };
}

The code above is valid, but not optimal. Because this particular function is recursive, a tail call return countdown.(n - 1); would optimize the call stack and improve performance significantly.

Tail calls also allow us to write more functional-style code. Void functions should not be treated any differently from non-void functions.

% signature of `List<T>#forEach`: `(List.<T>; callback: (T) => void) => void`

List.<int>(my_ints).forEach.((i) => print.(i));

% is better than:

List.<int>(my_ints).forEach.((i) {
    print.(i);
    return;
});