Function
A function is a block that takes an "argument", processes it, and returns it as a "return value". It is defined as follows.
add x, y = x + y
# or
add(x, y) = x + y
The names specified after a function name are called parameters.
In contrast, the objects passed to a function are called arguments.
The function add
is a function that takes x
and y
as parameters and returns the sum of them, x + y
.
The defined function can be called (applied/invoked) as follows.
add 1, 2
# or
add(1, 2)
Colon application style
Functions are invoked like f x, y, ...
, but if there are too many arguments for a single line, they can be applied using :
(colon).
f some_long_name_variable_1 + some_long_name_variable_2, some_long_name_variable_3 * some_long_name_variable_4
f some_long_name_variable_1 + some_long_name_variable_2:
some_long_name_variable_3 * some_long_name_variable_4
Codes above mean the same thing. This style is also useful when using if
functions, for example.
result = if Bool.sample!():
do:
log "True was chosen"
1
do:
log "False was chosen"
0
After :
, no code other than comments may be written, and must always be on a new line.
Also, you cannot use :
immediately after a function. Only do
and do!
can do this.
# NG
f:
x
y
# Ok
f(
x,
y
)
Keyword Arguments
If a function is defined with a large number of parameters, there is a danger of passing the arguments in the wrong order. In such cases, it is safe to call the function using keyword arguments.
f x, y, z, w, v, u: Int = ...
The functions defined above have many arguments and are arranged in a confusing order. You should not create such a function, but you may encounter such code when using code written by others. Therefore, we use keyword arguments. If you use keyword arguments, the values are passed from the name to the correct argument, even if they are in the wrong order.
f u := 6, v := 5, w:= 4, x := 1, y := 2, z := 3
Default parameters
Default parameters are used when some parameters are mostly fixed and you want to be able to omit them.
Default parameters are specified by :=
(default-assign operator). If base
is not specified, assign math.E
to base
.
math_log x: Ratio, base := math.E = ...
assert math_log(100, 10) == 2
assert math_log(100) == math_log(100, math.E)
Note that there is a distinction between specifying no argument and assigning None
.
p! x := 0 = print!
p!(2) # 2
p!() # 0
p!(None) # None
Can also be used with type specification and patterns.
math_log x, base: Ratio := math.E = ...
f [x, y] := [1, 2] = ...
However, within the default arguments, it is not possible to call the procedures (described later) or assign mutable objects.
f x := p! 1 = ... # NG
Also, the argument just defined cannot be used as the value passed to the default argument.
f x := 1, y := x = ... # NG
Variable-length arguments
The log
function, which outputs a log (record) of its arguments, can take any number of arguments.
log "Hello", "World", "!" # Hello World !
To define such a function, add *
to a parameter. This way, the function receives arguments as a variable-length array.
f *x =
for x, i ->
log i
# x == [1, 2, 3, 4, 5]
f 1, 2, 3, 4, 5
Function definition with multiple patterns
fib n: Nat =
match n:
0 -> 0
1 -> 1
n -> fib(n - 1) + fib(n - 2)
Functions like the one above, where match
appears directly under the definition, can be rewritten as follows.
fib 0 = 0
fib 1 = 1
fib(n: Nat): Nat = fib(n - 1) + fib(n - 2)
Note that a function definition with multiple patterns is not so-called overloading (multiple definition); a function has only a single definition. In the example above, n
must be of the same type as 0
or 1
. Also, as with match
, pattern matching is done from top to bottom.
If instances of different classes are mixed, the last definition must specify that the function argument is of type Or
.
f "aa" = ...
f 1 = ...
# `f x = ... ` is invalid
f x: Int or Str = ...
Also, like match
, it must also be exhaustive.
fib 0 = 0
fib 1 = 1
# PatternError: pattern of fib's parameter is not exhaustive
However, it can be made exhaustive by explicitly specifying the type using the refinement type described later.
fib: 0..1 -> 0..1
fib 0 = 0
fib 1 = 1
# OK
Recursive functions
A recursive function is a function that includes itself in its definition.
As a simple example, let us define a function factorial
that performs a factorial calculation. Factorial is a computation that "multiplies all positive numbers less than or equal to".
The factorial of 5 is 5*4*3*2*1 == 120
.
factorial 0 = 1
factorial 1 = 1
factorial(n: Nat): Nat = n * factorial(n - 1)
First, from the definition of factorial, the factorial of 0 and 1 are both 1.
In turn, the factorial of 2 is 2*1 == 2
, the factorial of 3 is 3*2*1 == 6
, and the factorial of 4 is 4*3*2*1 == 24
.
If we look closely, we can see that the factorial of a number n is the factorial of the preceding number n-1 multiplied by n.
Putting this into code, we get n * factorial(n - 1)
.
Since the definition of factorial
contains itself, factorial
is a recursive function.
As a reminder, if you do not add a type specification, it is inferred like this.
factorial: |T <: Sub(Int, T) and Mul(Int, Int) and Eq(Int)| T -> Int
factorial 0 = 1
factorial 1 = 1
factorial n = n * factorial(n - 1)
However, even if you can reason about it, you should explicitly specify the type of the recursive function. In the example above, a code like factorial(-1)
would work, but
factorial(-1) == -1 * factorial(-2) == -1 * -2 * factorial(-3) == ...
and this computation does not stop. Recursive functions must carefully define the range of values or you may end up in an infinite loop. So the type specification also helps to avoid accepting unexpected values.
High-order functions
A higher-order function is a function that takes a function as its parameter or return value. For example, a higher-order function that takes a function as its parameter can be written as follows
arg_f = i -> log i
higher_f(x: (Int -> NoneType)) = x 10
higher_f arg_f # 10
Of coursers, it is possible to take return value as a function.
add(x): (Int -> Int) = y -> x + y
add_ten = add(10) # y -> 10 + y
add_hundred = add(100) # y -> 100 + y
assert add_ten(1) == 11
assert add_hundred(1) == 101
By taking functions as parameters and return values in this way, more flexible expressions can be defined with functions.
Compile-time functions
A function name begins with an uppercase letter to indicate a compile-time function. User-defined compile-time functions must have all arguments as constants and must specify their types. Compile-time functions are limited in what they can do. Only constant expressions can be used in compile-time functions, i.e., only some operators (such as quadrature, comparison, and type construction operations) and compile-time functions. Arguments to be passed must also be constant expressions. In return, the advantage is that the computation can be done at compile time.
Add(X, Y: Nat): Nat = X + Y
assert Add(1, 2) == 3
Factorial 0 = 1
Factorial(X: Nat): Nat = X * Factorial(X - 1)
assert Factorial(10) == 3628800
math = import "math"
Sin X = math.sin X # ConstantError: this function is not computable at compile time
Compile-time functions are also used in polymorphic type definitions.
Option T: Type = T or NoneType
Option: Type -> Type
Compile-time function parameters must have different names from any constants already defined. If the names are the same, it will be interpreted as a constant pattern.
# Int is not a parameter but a constant (type Int)
K Int = None
Appendix: Function Comparison
Erg does not define ==
for functions. This is because there is no structural equivalence algorithm for functions in general.
f = x: Int -> (x + 1)**2
g = x: Int -> x**2 + 2x + 1
assert f == g # TypeError: cannot compare functions
Although f
and g
always return the same result, it is extremely difficult to make that determination. We have to teach algebra to the compiler.
So Erg gives up on function comparisons entirely, and (x -> x) == (x -> x)
also results in a compile error. This is a different specification from Python and should be noted.
# Python, weird example
f = lambda x: x
assert f == f
assert (lambda x: x) ! = (lambda x: x)
Appendix2: ()-completion
f x: Object = ...
# will be completed to
f(x: Object) = ...
f a
# will be completed to
f(a)
f a, b # TypeError: f() takes 1 positional argument but 2 were given
f(a, b) # TypeError: f() takes 1 positional argument but 2 were given
f((a, b)) # OK
The function type T -> U
is actually the syntax sugar of (T,) -> U
.