chharvey / counterpoint

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

The Blank Identifier #89

Closed chharvey closed 4 months ago

chharvey commented 1 year ago

This feature introduces a special identifier that may only be assigned. It is breaking, but only in a small way: _ is no longer a valid variable reference.

The blank identifier _ (a single underscore, U+005F LOW LINE) is a special identifier that may only be assigned and may never be referenced. Assigning a value to it can still generate compiler output, for example if evaluating the assigned value produces side-effects. The point of having a blank identifier is to pacify static analysis errors.

TLDR: We can assign a value to _ but we can never reference it in an expression.

let _: str = "unused";
_; % error

Motivational background. We’ll try to send a callback function into the forEach list method. Recall that forEach takes a function of type (T, int, T[]) => void as its argument. Our callback will only read the second parameter (the index).

List.<str>(["apple", "banana", "cherry"]).forEach.((item: str, index: int, source: str[]): void { % linter error!
    print.(index);
});
% expected: 0, 1, 2

linter error: parameters item and source are unused

Static analysis will complain about our callback because it doesn’t reference the item and source parameters. We can remove the source parameter (because a function of type (T, int) => void is assignable to type (T, int, T[]) => void), but we can’t remove the item parameter because that would change the order.

List.<str>(["apple", "banana", "cherry"]).forEach.((index: int): void { % type error
    print.(index);
});

type error: expression of type (int) => void is not assignable to type (str, int, str[]) => void

  • str is not a subtype of int (function parameter types are contravariant)

So the solution here is to declare a special parameter, the blank identifier _, as a placeholder for the unused parameter.

List.<str>(["apple", "banana", "cherry"]).forEach.((_: str, index: int): void { % no linter error
    print.(index);
});

This passes static analysis, and because the blank identifier can never be referenced, the linter doesn’t expect it to be used.

The same principle applies to destructuring variables:

let (_, b, c): str[3] = ["a", "b", "c"];
b; %== "b"
c; %== "c"
_; %> SyntaxError

Here, the blank identifier is treated more as a keyword than as an expression.

As a pseudo-keyword, the blank identifier can be assigned multiple times, without encountering duplicate declaration errors. Only the expressions are evaluated; no pointers are actually created.

List.<str>(["apple", "banana", "cherry"]).forEach.((_: str, index: int, _: str[]): void { % no error
    print.(index);
});
let (_, _, c, d): str[4] = ["a", "b", "c", "d"]; % no error

(Recall that normal variables would not allow that — e.g., let (x, x): str[2] = ["a", "b"]; would be an assignment error.)

The blank identifier is a valid record key since record keys may be any word including reserved keywords. However, as a valid key, it may not be declared multiple times.

let record1: [_: str, i: int] = [ % valid
    _= "hello world",
    i= 42,
];
let record2: [_: str, i: int, _: bool] = [ % assignment error
    _= "hello world",
    i= 42,
    _= "the answer", % assignment error
];

assignment error: Duplicate record key: _ is already set

The same applies to function arguments. We may assign an expression to a named argument, but not more than once.

my_forEach_callback.(_= "apple", index= 1, _= my_list); % assignment error

assignment error: Duplicate function argument: _ is already set

Note that even though we can send _= "apple" as a valid function argument, the function never references it!