Functions
Functions
Functions are packages of code that accomplish a specific task. Sometimes called subroutines or procedures in other languages, the intent is the same: to modularize code into simple, understandable and reusable components. They can also be used to model the mathematical notion of a function, a map from parameter values onto results.
Morpho provides a number of useful functions as standard, e.g. trigonometric functions, which are called by providing appropriate parameter values or arguments.
print cos(Pi/4)
When Morpho encounters a function call, control is transferred to the
function with the parameters initialized to the value of the supplied
arguments. Once the function has accomplished its task, it returns a
value that can be used by the code that called it. In our example, the
value of \(\cos(\pi/4)=2^{-1/2}\) is returned by the cos function and
then displayed by the print statement.
Function calls can occur anywhere where an expression is allowed, including as part of another expression or as an argument to another function call
print apply(cos, Pi/3 + arctan(1,0))
If the function is called without being used, the return value is simply discarded
cos(Pi/2)
To define a function, use the fn keyword. This must be followed by the
name of the function, a list of parameters enclosed in parentheses and
the function body, which is specified as a code block. Here's a
function that simply doubles its argument
fn twice(x) {
return 2*x
}
The return keyword, introduced above in Section
Return,-break,-continue, is used to introduce a
return statement that indicates where control should be returned to the
calling code. The expression after return becomes the return value of
the function. A function can contain more than one return statement
fn sign(x) {
if (x>0) {
return "+"
} else if (x<0) {
return "-"
} else return "0"
}
If no return statement is provided, the function returns nil by
default.
Functions can have multiple parameters. Here's another example that calculates the norm of a two dimensional vector
fn norm(x, y) {
var n2 = x^2 + y^2
return sqrt(n2)
}
When norm is called, the \(x\) and \(y\) values must be supplied in
order. These parameters are therefore referred to as positional
parameters. They take on their value from the order of the arguments
supplied. The calling code must call the function with the correct
number of positional parameters, otherwise an InvldArgs error is
thrown.
Optional parameters
Functions can also be defined with optional parameters, sometimes referred to as keyword or named parameters in other languages. Optional parameters are declared after positional parameters and must include a default value. This rather contrived example raises its argument to a power that can be optionally changed.
fn optpow(x, a=2) {
return x^a
}
If optpow is called with just one parameter
print optpow(3) // Expect: 9
the default value a=2 is used. But optpow can also be called
specifying a different value of a
print optpow(3, a=3) // Expect: 27
You can define multiple optional parameters as in this template to find a root of a specified function
fn findRoot(f, method=nil, initialValue=0, tolerance=1e-6) {
// ...
}
The caller can specify any number, including none, of the optional parameters so any of the following are valid
findRoot(f)
findRoot(f, tolerance=1e-8)
findRoot(f, tolerance=1e-6, initialValue=1)
Notice that the caller can supply optional parameters in any order; they
need not correspond to the order in which they're provided in the
function definition. The Morpho runtime automatically handles the
correct assignment. If positional parameters are defined, however, they
must be provided. Calling findRoot with no arguments would throw an
InvldArgs error.
There are some restrictions on the default value of optional arguments.
Currently, they may be any of nil, a boolean value, an Integer or a
Float. For other kinds of values, use nil as the default value and
check whether an optional argument was provided in the function. For
FindRoot, the method parameter probably refers to some object or class
that provides a user selectable algorithm. If no value of method is
provided, the function should select a default algorithm like so
fn findRoot(f, method=nil, initialValue=0, tolerance=1e-6) {
var m = method
if (isnil(m)) m = DefaultMethod()
// ...
}
Optional arguments are best used for functions that perform a complex action with many independent user-selectable parts, particularly those that may only needed infrequently. They alleviate the user of having to remember the order parameters must be given, and allow customization.
Variadic parameters
Occasionally it is useful for a function to accept a variable number of
parameters. A variadic parameter is indicated using the ... notation,
as in this example that sums its arguments:
fn sum(...x) {
var total = 0
for (e in x) total+=e
return total
}
When called, the parameter x is initialized as a special container
object containing the arguments provided. It's valid to call sum with
no arguments provided, in which case the container x is empty.
You may only designate one variadic parameter per function. Hence this example
fn broken(...x, ...y) { // Invalid
}
throws a OneVarPr error when compiled.
It's possible to combine positional and variadic parameters, as in this example that computes the \(L_{n}\) norm of its parameters (at least for \(n\ge2)\)
fn nnorm(n, ...x) {
var total = 0
for (e in x) total+=e^n
return total^(1/n)
}
When a function that accepts both positional and variadic parameters is
called, the required number of argument values are assigned to
positional parameters first, and then any remaining arguments are
assigned to the variadic parameter. Hence, you must call a function with
at least the number of positional parameters. Calling nnorm with no
parameters will throw a InvldArgs error.
It's also required that the variadic parameter, if any, comes after positional parameters. This example
fn broken(...x, y, z) { // Invalid
}
would throw a VarPrLst error on compilation.
Optional parameters must be defined after any variadic parameter. We
could redefine nnorm to make it compute the \(L_{2}\) norm by default
like this
fn nnorm(...x, n=2) {
var total = 0
for (e in x) total+=e^n
return total^(1/n)
}
Multiple dispatch
While Morpho functions, by default, accept any value for each parameter, it's often the case that a function's behavior depends on the type of one or more of its arguments. You can therefore define functions to restrict the type of arguments accepted, and you can even define multiple implementations of the same function that accept different types. The correct implementation is selected at runtimethis is known as multiple dispatchdepending on the actual types of the caller. It is sometimes clear to the compiler which implementation will be called, in which case the compiler will select this automatically. Morpho implements multiple dispatch efficiently, and so the overhead of this relative to a regular function call is small.
Consider this skeleton intended to compute the gamma function, a mathematical special function that is related to factorials for integer values, and which is also defined for the complex plane:
fn gamma(Int x) {
if (x>0) return Inf
return factorial(x-1)
}
fn gamma(Float x) {
// An implementation for Floating point numbers
}
fn gamma(Complex x) {
// Another implementation
}
When gamma is called, one of the three implementations is selected
depending on whether the argument is an Integer, Float and Complex
number. If no implementation is available, the MltplDsptchFld error is
thrown. This could happen, for example, if gamma is called with a List
by mistake. Implementations need not have the same number of arguments.
Here's a collection of implementations that return the number of
arguments provided:
fn c() { return 0 }
fn c(x,y) { return 1 }
fn c(x,y,z) { return 2 }
fn c(x,y,z,w) { return 3 }
Multiple dispatch can occur on any combination of positional parameters and not all positional parameters need to be typed. The ordered collection of types accepted by the positional arguments of an implementation is known as its signature. This enterprising collection joins Strings to Lists, producing a String.
fn join(String x, List y) { return x + String(y) }
fn join(String x, String y) { return x + y }
fn join(List x, String y) { return String(x) + y }
fn join(List x, List y) { return String(x) + String(y) }
It's an error to define two implementations with the same signature within the same scope.
Documentation
We highly recommend documenting each function with a comment before (or close to) the function definition. The set of parameters is known as the interface of the function, and it's recommended to document the meaning and purpose of each parameter, as well as any restrictions, constraints or required units. There are many valid styles to accomplish this, but the importance of documenting interfaces cannot be emphasized enough.