# User-Defined Functions: Arguments, Shapes, and Error Messages #

It's possible to construct user-defined functions that take in scalar/vector/matrix arguments, and produce a scalar/vector/matrix appropriately.

## Example #

Suppose we have a `MatrixGrader`

problem in which we want to provide students with a function `rot(vector, axis, angle)`

that rotates a vector about a given axis by a given angle. We can provide such a function with the `user_functions`

configuration key.

```
>>> import numpy as np
>>> from mitxgraders import *
>>> def rot(vec, axis, angle):
... """
... Rotate vec by angle around axis. Implemented by Euler-Rodrigues formula:
... https://en.wikipedia.org/wiki/Euler-Rodrigues_formula
...
... Arguments:
... vec: a 3-component MathArray to rotate
... axis: a 3-component MathArray to rotate around
... angle: a number
... """
... vec = np.array(vec)
... unit_axis = np.array(axis)/np.linalg.norm(axis)
... a = np.cos(angle/2)
... omega = unit_axis * np.sin(angle/2)
... crossed = np.cross(omega, vec)
... result = vec + 2*a*crossed + 2*np.cross(omega, crossed)
... return MathArray(result)
>>> grader_1 = MatrixGrader(
... answers='rot(v, [0, 0, 1], theta)',
... variables=['v', 'theta'],
... sample_from={
... 'v': RealVectors(shape=3),
... },
... user_functions={
... 'rot': rot
... }
... )
```

### The Problem #

Our `rot(vec, axis, angle)`

function works, but if students supply the function above with arguments of incorrect type, they receive unhelpful error messages:

```
>>> try:
... grader_1(None, 'rot(v, theta, [0, 0, 1])')
... except StudentFacingError as error:
... print(error)
There was an error evaluating rot(...). Its input does not seem to be in its domain.
```

### The Solution #

To provide students with more useful error messages, we can use `specify_domain`

, a decorator function imported from `mitxgraders`

. Decorator Functions are "higher-order functions" that take functions as input and produce functions as output, usually modifying the input function's behavior. In our case, `specify_domain`

will modify the behavior of `rot`

so as to provide more helpful `StudentFacingError`

s.

Here we go:

```
>>> @specify_domain(input_shapes=[[3], [3], [1]], display_name='rot')
... def rot_with_error_messages(vec, axis, angle):
... # rot(vec, axis, angle) defined above
... return rot(vec, axis, angle)
>>> # Define new grader using rot_with_error_messages
>>> grader_2 = MatrixGrader(
... answers='rot(v, [0, 0, 1], theta)',
... variables=['v', 'theta'],
... sample_from={
... 'v': RealVectors(shape=3),
... },
... user_functions={
... 'rot': rot_with_error_messages
... }
... )
```

Now if a student calls `rot`

with incorrect inputs, they receive a more helpful message:

```
>>> try:
... grader_2(None, 'rot(v, theta, [0, 0, 1])')
... except StudentFacingError as error:
... print(str(error).replace('<br/>', '\n'))
There was an error evaluating function rot(...)
1st input is ok: received a vector of length 3 as expected
2nd input has an error: received a scalar, expected a vector of length 3
3rd input has an error: received a vector of length 3, expected a scalar
```

### Configuring specify_domain #

The decorator `specify_domain`

accepts optional keyword arguments and should be called in either of two equivalent ways:

```
>>> @specify_domain(keyword_arguments) # doctest: +SKIP
... def target_function(x, y, z):
... pass # do whatever you want
>>> # or, equivalently:
>>> def target_function(x, y ,z):
... pass # do whatever you want
>>> decorated_function = specify_domain(keyword_arguments)(target_function) # doctest: +SKIP
```

The keyword arguments are:

`input_shapes`

: A list that indicates the shape of each input to the target function. This list**must**have the same length as the number of arguments in the target function. Each list element should be one of the following:`1`

: indicates input is scalar`k`

(positive integer > 1): indicates input is a k-component vector`[k1, k2, ...]`

, list of positive integers: means input is an array of shape (k1, k2, ...)`(k1, k2, ...)`

, tuple of positive integers: equivalent to`[k1, k2, ...]`

`'square'`

(string): indicates a square matrix of any dimension

`display_name`

(str): Function name to be used in error messages. Defaults to`None`

, meaning that the function's`__name__`

attribute is used.

So, for example,

```
>>> @specify_domain(input_shapes=[1, [3, 2], 4], display_name='myfunc')
... def some_function(x, A, v):
... pass
```

specifies that the function `some_func`

must be called with three arguments:

- 1st argument: scalar,
- 2nd argument: 3 by 2 matrix, and a
- 3rd argument: 4-component vector.

### Arbitrary Same-Shape Arguments #

Some functions may allow an arbitrary number of arguments to be passed in. For example, consider a user-defined minimum function:

```
>>> def my_min(*args):
... return min(*args)
```

To inform `specify_domain`

that a function should accept arbitrarily many arguments of a certain shape, supply a single shape to `input_shapes`

, and also pass in a `min_length`

parameter, to specify the minimum number of arguments required. (If you specify `min_length`

and have more than one shape in `input_shapes`

, a `ConfigError`

will result.) So, our `my_min`

function can be decorated as follows:

```
>>> @specify_domain(input_shapes=[1], display_name='min', min_length=2)
... def my_min(*args):
... return min(*args)
>>> my_min(1.5, 2.3, 4.6)
1.5
>>> try:
... my_min(1)
... except StudentFacingError as error:
... print(error)
Wrong number of arguments passed to min(...): Expected at least 2 inputs, but received 1.
>>> try:
... my_min(MathArray([1, 2]), MathArray([3, 4]))
... except StudentFacingError as error:
... print(error)
There was an error evaluating function min(...)
1st input has an error: received a vector of length 2, expected a scalar
2nd input has an error: received a vector of length 2, expected a scalar
```