Matrices and Functions in M

Whiteknight on 2009-03-15T14:04:23

Continuing my short tutorial about M, today I'm going to talk about variables and functions in M. This is, simultaneously, one of the hardest parts of the Matrixy compiler for us to get right, and we've just made our second attempt at it.

Functions in M are named and called very similarly to how functions are called in other languages like C or Perl:

foo(1, 2, 3);

Again, as we covered last time, omitting the trailing semicolon will print the return value of the function to the console, if any. As we'll see later, functions always take variadic argument lists by default, and it's up to the function itself to keep track of it's own input arguments. Functions can also return multiple values:

[a, b, c] = foo(1, 2, 3);

And again, it's up to the function to recognize how many return values are expected and alter it's behavior appropriately. There is no multiple dispatch in M, the function implements it's own logic internally to determine the configuration of input and output parameters and adjust it's behavior to that.

Matrices, as we saw previously, are very important to M. It originated as a linear algebra mathematics pack, after all. We index into a matrix variable in the same way that we call a function:

x = [1 2 3 4]; x(1) % ans = 1

This causes obvious problems with our parser at compile time because we have to determine whether "foo(1)" is a function call or a variable index. To make things a little more difficult, a bare identifier could also be either a variable or a function call with no arguments:

help; % Calls the "help" function with no args x; % the variable x. store it's value in ans

So, as you can see, there is a lot of ambiguity that needs to be resolved at runtime when the parser finds an identifier bar:

1) If a variable bar has been defined locally, treat it like a variable and return it's value. Variables of the same name always overshadow functions. 2) If we have a user-defined function bar in the current scope, call that. 3) If there is a builtin function bar, call it 4) If all else fails, search the path for a file named "bar.m". this file should contain either a defintion for function bar, or it should be a "script file" which takes no parameters but is executed directly.

This is all made more difficult because M allows function handles to be stored in variables and called as functions:

y = @foo; % handle to function foo y(1, 2); % call function foo(1, 2)

or even anonymous functions to be defined:

x = @(r) 2 * pi * r; x(3); % 2 * pi * 3;

So even if baz is a variable as determined by rule #1 in the dispatch algorithm above, it could still cause the invocation of a function if it's a function handle.

Functions also have the ability to be executed using bare word arguments:

help foo

is the same as the call

help('foo')

The idea behind the ambiguity between variable and function dispatching, at least as I've heard it, is that it facilitates the interchange of pure functions and lookup tables. Either can be implemented, and the caller doesn't have to worry about which type it is.

Another point to be aware of is that M traditionally doesn't have namespaces, so all functions are visible by default at all time. This results in a large amount of namespace pollution, but also appears to be a driving force behind the multistage dispatch algorithm I showed above: There are far too many identifiers defined by default to prohibit the user from overriding any of them locally.

One last thing to mention is that if we absolutely need to call a function instead of indexing into a variable of the same name, we can use the feval() function. feval() takes the name of a function and a variadic list of arguments and calls that function, never indexing into a variable:

x = [1 2 3]; feval("x", 2); % function x, not variable x

So this has been a brief introduction to function and variable use in M. It should demonstrate that there is some significant difficulty on the part of the compiler-designer, but it creates an interesting situation for end users to interchange functions and lookup tables, and to silently override the huge library of builtin functions with their own versions of them if needed.

Matrixy Compiler for Parrot