Functions as Data
Functions as data
Functions in Morpho are objects just like Lists, Strings, etc. Hence, they can be assigned to variables, and called at a later point
var P = sin
print P(Pi/10)
Functions can also be stored in collections. This example computes the value of several trigonometric functions, which are stored in a list:
var lst = [sin, cos, tan]
for (f in lst) print f(Pi/3)
Finally, functions can be passed as arguments to other functions. For example, here's a function that applies a given function twice to its second argument
fn calltwice(f, x) {
return f(f(x))
}
You can then use calltwice with any function, as in this example:
fn sqr(x) {
return x^2
}
print calltwice(sqr, 2)
Anonymous functions
Morpho provides an abbreviated syntax for functions that can be used
in assignments or as parameters. There's no need to give the function a
namesuch functions are hence called anonymousand you may, optionally,
provide a single statement as the body in place of the usual code block.
The value of the body statement is returned from the function as if a
return was in front of it. Hence
var sq = fn (a) a^2
is equivalent to the named function
fn sqr(a) {
return a^2
}
The anonymous function syntax is particularly useful for supplying to
other functions, because quite often such functions end up being quite
short. The List class, for example, provides a sort method that can be
used to sort the contents. You can optionally provide a sort function
that compares two elements of the list \(a\) and \(b\); this function should
return a negative value if \(a<b\), a positive value if \(a>b\) and \(0\) if
\(a\) and \(b\) are equal. This example sorts the list in reverse order
var lst = [5,2,8,6,5,0,1,3,4]
lst.sort(fn (a,b) b-a)
print lst
In some languages, anonymous functions are referred to as lambda functions, referring to the pioneering work of Alonzo Church on the theory of computation.
Scope
Functions obey scope so functions can be defined locally within a code block as in this example
{
fn f(x) { return x^2 }
print f(2) // prints 4
}
print f(2) // Raises an error
The function f remains available for the rest of the code block, but
is not visible outside of it. Hence, while the first call works, the
second throws SymblUndf as f is no longer visible.
Closures
Functions can be returned from other functions; such functions are known as closures for reasons that will become apparent shortly. Here's an example
fn inc(a) {
fn f(x) { return x + a }
return f
}
var add = inc(5)
print add(10) // prints 15
The function inc manufactures a closure that adds a given value a to
its argument. This can be a bit complicated to follow, so let's trace
out the sequence of events:
-
incis called with the argument5. During the call, the parameteratakes on this value inside theincfunction. -
A closure using the local function
fis created withinincusing the provided value ofa(i.e. 5). -
The closure is returned to the calling code. The value of
aremains available to the closure. -
The user calls the closure with the argument
10; the closure adds5to this and returns15which is displayed.
Closures are so-named because they enclose the environment in which
they're created. In this example, the value of a is encapsulated
together with the function f, forming the closure. The quantity a as
is sometimes called an upvalue, because it's not local to the function
definition; a is said to be captured by the closure.
Upvalues can be written to as well as read from. This closure reports how many times it has been called
fn counter(val) {
fn f() { val+=1; return val }
return f
}
var c = counter(0)
print c() // prints 1
print c() // prints 2
print c() // prints 3
Closures can be called anywhere regular functions can. An important use of closures is to create functions that obey a defined interface, i.e. they have the same signature, but have access to additional parameters. This example creates a function that describes the electric scalar potential due to a point charge with given charge and position
fn scalarPotential(q, x0, y0) {
fn phi(x,y) {
return q/sqrt((x-x0)^2 + (y-y0)^2)
}
return phi
}
var p1 = scalarPotential(1, -1, 0)
var p2 = scalarPotential(-1, 1, 0)
print p1(0,1) + p2(0,1)
The closures created can then be called with position of interest, returning the appropriate value. This could be useful in a larger code, where potential functions for many different types of entity are to be created, but each type requires very different data to specify them. Nonetheless, because all such potential functions obey the same interface, they can be used interchangeably.