Partial functions
You can use a wildcard symbol *
to create a reference to a function, that is, to obtain a value of a function type
that allows you to call the function:
function f(x: integer, y: integer) = x * y;
val g = f(*); // Type of "g" is (integer, integer) -> integer
g(123, 456); // Invocation of f(123, 456) via "g".
The f()
notation is a partial application of a function. It allows specifying values for some parameters and wildcards
*
for others, returning a value of a function type accepting the parameters for which wildcards were specified:
val g = f(123, *); // Type of "g" is (integer) -> integer
g(456); // Calls f(123, 456).
val h = f(*, 456); // Type of "h" is (integer) -> integer
h(123); // Calls f(123, 456).
Details of partial application syntax
If a wildcard symbol *
is specified for at least one parameter, all the unspecified parameters that don't have default
values are also considered wildcards. Thus, the exact value of type (integer, integer) -> integer
.
Wildcard symbol *
specified as the last parameter without a name has a special meaning. It doesn't correspond to a
particular function parameter but determines that the call expression is a partial application. Thus, it's unnecessary
to specify *
for each function parameter. It's enough to write f(*)
, regardless of the number of parameters f
has
(even if it's zero).
Suppose a wildcard without a name is specified as the last parameter. In that case, there must be no other wildcard parameters because otherwise, the previous one isn't needed (as the call is already a partial application) and may be confusing.
Parameters with default values that aren't explicitly specified as wildcards are bound to their default values. The default values are calculated at the moment of partial application. Consider the following function:
function p(x: integer, y: integer = 1) = x * y;
The following expressions return the same value of type (integer) -> integer
:
-
p(*)
-
p(x = *)
-
p(x = *, y = 1)
The code to include both parameters into a function type that is to get (integer, integer) -> integer
:
p(y = *)
p(x = *, y = *)
Note, for instance, that rules 1 and 2 imply that for a single-parameter function with a default value:
function r(x: integer = 123): integer { ... }
-
r(*)
returns() -> integer
(as the last unnamed*
isn't assigned to a particular parameter) -
r(x = *))
returns(integer) -> integer
The order of named wildcard parameters matters. Consider the following function:
function f(x: integer, y: text, z: boolean): decimal { ... }
f(*)
is equivalent tof(x = *, y = *, z = *)
and returns(integer, text, boolean) -> decimal
f(z = *, y = *, x = *)
returns(boolean, text, integer) -> decimal
f(y = *)
is equivalent tof(y = *, x = *, z = *)
and returns(text, integer, boolean) -> decimal
f(*, z = *)
is equivalent tof(x = *, z = *, y = *)
and returns(integer, boolean, text) -> decimal
Partial application of system functions
Most system library functions can be partially applied to turn them into function values. However, some library functions are overloaded, for example:
-
abs(integer): integer
-
abs(decimal): decimal
You must know the type of the function value to partially apply an overloaded function:
val f: (integer) -> integer = abs(*); // The type of variable "f" allows to determine which "abs" to use.
Member functions (methods of types text
,list<T>
, etc.) can be partially-applied too:
val l = [5, 6, 7];
val f = l.size(*); // Type of "f" is () -> integer.
print(f()); // Prints 3.
l.add(8);
l.add(9);
print(f()); // Prints 5.
Some system functions don't support partial application: print()
, log()
, require()
, text.format()
, and so on.