Skip to content

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.