Function Definitions with Typed Arguments using Patterns

Thomas H. Meyer

Department of Natural Resources and the Environment
University of Connecticut
thomas.meyer@uconn.edu

Learning objectives

This is a Mathematica software engineering workshop exploring how to

define valid domains for values

do type checking in your own function definitions

leverage the unique capabilities of Mathematica in this regard

Audience

This workshop is for people who

alreadry write their own functions

want to trap and report runtime errors

want to write packages, or improve them

Type Domains

A variable is typed if it is only allowed to store values that are elements of a specified domain

Examples of domains in typical procedural languages include

integers

reals

characters

strings of text

but Mathematica allows much more sophisticated domains:

lists

lists of numeric quantities (such as a matrix of complex numbers)

reals in the range (-∞ < 𝕣 ≤ π/2]

Why typing is important

Mathematica has dynamic typing

Many programming languages, but not Mathematica, have typed variables that are declared at compile time and that are not allowed to change type during run time. For example, in the C programming language,

int c;

declares that the variable named c holds a machine-precision integer throughout the execution of the program. Mathematica has no such restrictions. Typing is dynamic; a symbol's type follows the type of the last thing assigned to it, if any, or is Symbol, if nothing.

Here c is assigned an integer value (or, c is storing an integer)

c=6

Now c is a function

c=Sin

c[7.3]

This syntax removes whatever value was assigned to c, but c still exists and can participate fully in symbolic expressions.

c=.

c

6c/6

Dynamic typing is sometimes a problem

A variable is typed if it is allowed to store values that are elements of a specified domain

Assigning a type to a variable is effective for helping programmers prevent type clashes in their programs. For example, attempting to add the string "16" to the integer 5 is probably a bug, and a typed language would catch this mistake at compile time. In one sense, Mathematica allows anything to be added to anything else; the following does not generate any kind of error message

"16"+5

This output is mystifying. Why didn't Mathematica perform the addition? Since Strings are not formatted with quotes in the output, the String "16" looks like the integer 16, and confusion reigns. However, using FullForm reveals the inner truth

FullForm["16"+5]

This example is contrived; however, it is not far fetched to imagine a complicated function that returned an apparently unevaluated expression because somewhere deep inside there was an arithmetic operation applied to a String.

Mathematica gives us the flexibility to apply operators to any kind of operand. While such flexibility is actually a strength in certain circumstances, it is usually a bug in Mathematica as often as it is with any other programming language. Fortunately, Mathematica provides mechanisms to enforce type checking when it is beneficial, and to accomplish much more besides.

Patterns

A pattern is a Mathematica construct that captures patterns. Patterns appear frequently in function definitions. Functions are defined thus

f[pattern]:=body

and the function operates only on arguments that match the pattern; otherwise, the value of the expression is the function itself.

Examples

Here the pattern is the literal integer 6

f[6]:="Hello There!"

f[6]

We are free to invoke f with any other argument we please, but at this point, only f[6] does anything we want

{f[],f[t],f[99]}

Here the pattern is any integer

f[i_Integer]:=i^2

f[7]

f[7.]

Notice that f[6] still has the behavior defined first even though 6 is an integer

f[6]

We'll use patterns to define domains for anything we want

Type information in Mathematica Expressions

All Mathematica expressions have type information, it's just hidden!

Head

Mathematical objects carry their type information in their Head

Head[16]

Head["16"]

Head[5+"16"]

FullForm[5+"16"]

Head[4/5]

Head[24.5]

Head[6+I 9]

Head[Plot[Sin[x],{x,0,1}]]

Head[{3,67,1}]

A head can be extracted with the Head function or with a somewhat peculiar application of the Part function: the head is the (0^th) part of any object

Head[16]

Part[16,0]

16[[0]]

Blank (underscore)

We can make good use of the Head information to accomplish typing. We'll do this with the underscore character _ which is an operator. It is named Blank.

?Blank

Blank is the tool we will use to add typing to our function definitions as appropriate.

myFunc[x_,y_]:=x+y

The Blanks in the myFunc Definition mean that x is the name of the first argument and it can be of any type (that is, have any Head) at all; y is the name of the second argument and it, too, can be any type at all.

Matching heads with Blanks

Blanks, conveniently, are easy to use for matching specific heads. First, we need to remove any definitions of myFunc for reasons that will be made clearly later.

Remove[myFunc]

Putting the name of a head after the Blank causes the Blank to only match Mathematica objects with that head.

myFunc[x_Integer,y_Integer]:=x+y

Now myFunc only accepts integer arguments

myFunc[16,5]

If even one argument does not match the pattern, the function returns itself as an unevaluated expression

myFunc["16",5]

Function overloading

We can define another version of myFunc that accepts Reals for arguments:

myFunc[x_Real,y_Real]:=x+y

myFunc[16.,5.]

There really are two versions of myFunc known to the kernel

??myFunc

It is called overloading when a function has more than one implementation. Mathematica decides which one to call by finding the one whose pattern matches the arguments. It's possible that more than one pattern could work (check for Integer and check for 6). In that case, the functions are chosen in the order of greatest specificity.

Blanks and predicates: argument domains

We would like to have a version of myFunc that works for any kind of numeric inputs whatsoever, and it's rather tedious to enumerate all the possible combinations of Integer, Rational, Real, and Complex. Fortunately, Blank can be followed by a predicate that computes whether an argument is valid or not.

Remove[myFunc];
myFunc[x_?NumericQ,y_?NumericQ]:=x+y

myFunc[6,5.5]

myFunc[6+I 10,5.5]

Blank can be followed by a Head and the ? can be followed by a predicate -- any predicate. Here's an example that checks type and range

myFunc[x_Integer?Positive,y_Integer?Positive]:=x y

myFunc[5,5]

myFunc[-5,5]

Here are the built-in predicates - any function whose name ends in a Q

?System`*Q

If these aren't good enough, you can provide your own pure function. Here is a check for geodetic latitude -π≤ϕ≤π
For now, I first write a predicate using the Mathematica template.

This function returns True iff ϕ is in the proper range and of the proper type.

latitudeQ[ϕ_]:=NumericQ[ϕ]&&-π≤ϕ≤π

latitudeQ[π]
latitudeQ[Plot[x,{x,0,1}]]
latitudeQ[-20]
latitudeQ[]
latitudeQ[π,4]

This function returns False for any other circumstance: ϕ is outside the proper range, ϕ is the wrong type, the wrong number of arguments were given, etc.
IT IS IMPORTANT TO PROVIDE SUCH A FUNCTION THAT IS THE POSITIVE ASSERTION OF FAILING THE PREDICATE.

latitudeQ[___]:=False

latitudeQ[π]
latitudeQ[Plot[x,{x,0,1}]]
latitudeQ[-20]
latitudeQ[]
latitudeQ[π,4]

These two functions will return either True or False for any input. This is how predicates should be crafted. 
Here's the main function. It uses latitudeQ as the domain of its argument.

goodLat[ϕ_?latitudeQ]:=ϕ

goodLat[0]

goodLat[10]

goodLat["Hi"]

We can, of course, use a pure function instead of defining a predicate to test the validity of individual arguments

Remove[goodLat];
goodLat[ϕ_?(-π≤#≤π&)]:=ϕ

goodLat[0]

goodLat[10]

domains involving more than one argument

The predicate can be applied to more than one argument as follows

Remove[myFunc];
myFunc[x_?NumericQ,y_?NumericQ]/;x<y:=x+y

myFunc[5,9]

myFunc[9,5]

When none of the patterns match: error handling

If a function is invoked with arguments that match no pattern, then the invocation itself is returned

myFunc[9,5]

Providing a "matches all" definition can be used to trap errors and report helpful messages:

myFunc[x_,y_]:=(Message[myFunc::badArgs];Null)

myFunc::badArgs="call with two numeric inputs, please"

myFunc[9,5]

We can report the input values back to the user (which is usually us)

myFunc[x_,y_]:=(Message[myFunc::badArgs,x,y];Null)

myFunc::badArgs="call with two numeric inputs, please (`1`), (`2`)"

myFunc[9,5]

Patterns that match more than one thing

There are three types of blanks that look very similar but act rather differently. One Blank matches exactly one thing.

myFunc[5,]

Two blanks match one or more things

Remove[myFunc]
myFunc[x_?NumericQ,y_?NumericQ]:=x+y
myFunc[args__]:=(Message[myFunc::badArgs,{args}];Null)
myFunc::badArgs="call with two numeric inputs. you entered (`1`)";

myFunc[5,]

myFunc[5,6,7,8]

But two blanks don't match nothing

myFunc[]

but three Blanks will

Remove[myFunc]
myFunc[x_?NumericQ,y_?NumericQ]:=x+y
myFunc[args___]:=(Message[myFunc::badArgs,{args}];Null)
myFunc::badArgs="call with two numeric inputs. you entered (`1`)";

myFunc[]

Looking for Literals

moveCar["red"]:="The light is red"
moveCar["green"]:="The light is green"
moveCar[_]:="Do something else"

moveCar["red"]

moveCar[555]

Enumerated types

semiMajorAxis["GRS80"]:=6378137

semiMajorAxis["GRS80"]

Works with symbols

moveCar[red]:="STOP"

moveCar[red]

It only works so long as the argument does not evaluate to something other than its symbol

red=4
moveCar[red]

Fancy examples

Vector addition of two numeric vectors having exactly two elements each

myVecQ[{_?NumericQ,_?NumericQ}]:=True
myVecQ[___]:=False

myVecQ[{1,5}]

It's handy having the predicate function, it eliminates retyping the long predicate expression

Remove[myFunc]
myFunc[x_?myVecQ,y_?myVecQ]:=x+y

myFunc[{5,6},{2,8}]

myFunc[{5,6,3},{2,8}]

How about vector addition for vectors of any length greater than zero, but the inputs must have the same length. The two periods mean "one or more of these things"

myGenVecQ[{_?NumericQ..}]:=True
myGenVecQ[___]:=False

myGenVecQ[{1}]

myGenVecQ[{1,1,1,1,3,34,4}]

This example establishes the domains of the arguments and then checks whether their lengths are equal, something that is not possible in the argument checking.

myFunc[x_?myGenVecQ,y_?myGenVecQ]/;Length[x]==Length[y]:=x+y

myFunc[{1,1,1,1,3,34,4},{1,1,1,1,3,34,4}]

myFunc[{x},{t,v}]

Using unevaluated symbols

Blanks can match unevaluated symbols thus

myFunc[f_Symbol,g_Integer]:=Print[f," is a symbol"]

myFunc[x,5]

Here's a function that behaves differently if the arguments are symbols or if they're numeric.
First, let's define a function that embodies the mathematical expression of interest. This is called the radius of curvature in the prime vertical. Notice that the arguments are not specified in any way.

RNExpr[a_,ϵ_,ϕ_]:=(a/(Sqrt[1-(ϵ^2)((Sin[ϕ])^2)]))

Now, define a user function whose arguments must all be Symbols, and call RNExpr with them:

RN[a_Symbol,ϵ_Symbol,ϕ_Symbol]:=RNExpr[a,ϵ,ϕ]

RN[a,ϵ,ϕ]

Of course, it doesn't matter what symbols we use.

RN[RefEllipA,Ecc,GeoLat]

Invoking the function with numeric arguments causes the expression to be evaluated, returning a numeric result

RN[a_?NumericQ,ϵ_?NumericQ,ϕ_?NumericQ]:=RNExpr[a,ϵ,ϕ]

RN[semiMajor[],(Sqrt[eSq[]]),42°]

This approach is probably more useful in packages. The general, unspecified function would not be exposed to the package's users, but the functions with typing information would be. This is one way to (i) create self-documenting code (ii) provide the expression for symbolic computations (iii) provide numeric results, and (iv) not duplicate any code.

Law of cosines example

This is a somewhat more complicated, and probably more useful, example based on the planar law of cosines:
cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b))

In "practice," the LOC can reasonably be used given the angle and two sides from which the third side is to be found, or given three sides from which the angle is to be found. Let's write a single function to do all of that.

This definition automatically determines that we're looking for the angle given three sides

LOC[θ_Symbol,a_?NumericQ,b_?NumericQ,c_?NumericQ]:=ArcCos[(((a^2)+(b^2)-(c^2))/(2a b))]

LOC[α,50,60,70.]/°

This definition automatically determines that we're looking for the side opposite the angle given the adjacent sides and the angle

LOC[θ_?NumericQ,a_?NumericQ,b_?NumericQ,c_Symbol]:=(Sqrt[-2a b Cos[θ]+(b^2)+(a^2)])

LOC[78.463°,60,70.,c]

We can do the algebra for all four cases, but Mathematica solve the equation for us

Remove[LOC]
LOC[θ_Symbol,a_?NumericQ,b_?NumericQ,c_?NumericQ]:=Solve[Cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b)),θ]
LOC[θ_?NumericQ,a_Symbol,b_?NumericQ,c_?NumericQ]:=Solve[Cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b)),a]
LOC[θ_?NumericQ,a_?NumericQ,b_Symbol,c_?NumericQ]:=Solve[Cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b)),b]
LOC[θ_?NumericQ,a_?NumericQ,b_?NumericQ,c_Symbol]:=Solve[Cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b)),c]

LOC[α,50,60,70.]
LOC[78.463°,60,70.,c]

This is cool, but the rules and the negative roots are annoying. Also, let's use a single function rather than repeat the Solve four times

Remove[LOC]
LOCBase[θ_,a_,b_,c_,var_Symbol]:=Max[var/.Solve[Cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b)),var]]

LOC[θ_Symbol,a_?NumericQ,b_?NumericQ,c_?NumericQ]:=LOCBase[θ,a,b,c,θ]
LOC[θ_?NumericQ,a_Symbol,b_?NumericQ,c_?NumericQ]:=LOCBase[θ,a,b,c,a]
LOC[θ_?NumericQ,a_?NumericQ,b_Symbol,c_?NumericQ]:=LOCBase[θ,a,b,c,b]
LOC[θ_?NumericQ,a_?NumericQ,b_?NumericQ,c_Symbol]:=LOCBase[θ,a,b,c,c]

LOC[α,50,60,70.]
LOC[78.463°,a,50.,50]
LOC[78.463°,60,b,70.]
LOC[78.463°,60,70.,c]

One last refinement: have Mathematica find the symbol for us. The braces around var take care of Select always returning a list.

Remove[LOC]
LOCBase[θ_,a_,b_,c_,{var_Symbol}]:=Max[var/.Solve[Cos[θ]==(((a^2)+(b^2)-(c^2))/(2a b)),var]]

LOC[θ_,a_,b_,c_]:=LOCBase[θ,a,b,c,Select[{θ,a,b,c},Head[#]===Symbol&]]

LOC[α,50,60,70.]
LOC[78.463°,a,50.,50]
LOC[78.463°,60,b,70.]
LOC[78.463°,60,70.,c]

Thomas H. Meyer

Department of Natural Resources and the Environment
University of Connecticut
thomas.meyer@uconn.edu