Difference between revisions of "Modifying Contexts (advanced)"

From WeBWorK_wiki
Jump to navigation Jump to search
 
(25 intermediate revisions by 5 users not shown)
Line 1: Line 1:
= Context Modification: How to Modify an Existing Context or Make Your Own Context =
 
  +
== Advanced Context Modifications ==
   
This document explains how to modify the current context and how to define your own context. You might want to do the latter if the problems you write/use always have certain nonstandard characteristics. For example, maybe your book always uses a certain parentheses type, etc.
 
  +
The [[Introduction to Contexts]] describes how to make basic modifications to a Context's [[Introduction to Contexts#Variables|variables]], [[Introduction to Contexts#Constants|constants]], [[Introduction to Contexts#Strings|strings]], [[Introduction to Contexts#Flags|flags]], [[Introduction to Contexts#Functions|functions]], [[Introduction to Contexts#Operators|operators]], and [[Introduction to Contexts#Reduction Rules|reduction rules]]. Here we will describe more advanced modifications and techniques involving the Context.
   
First we will describe common context modifications which you can make. The second part of the document will explain how to combine these changes into a file to define your own custom context.
 
  +
=== Number Formats ===
=== Part I. Modifying an Existing Context ===
 
   
What can you change? Here is a description of some of the basic changes you can make, what the options are, and how to change those values. You can do a lot of customization here, probably more than you need, but more advanced modifications can be made if you know how to program in Perl.
 
  +
Real numbers are stored using a format that retains about 16 or 17 significant digits, making computations very accurate in most situations. When a number is displayed, you probably don't want to see all 17 digits (that would make a vector in three-space take up around 35 characters, for example). To make answers easier to read, MathObjects usually display only 6 significant digits. You can change the format used, however, to suit your needs. The format is determined by the <code>Context()->{format}{number}</code>, which is a <code>printf</code>-style string indicating how real numbers should be formatted for display.
==== (1) Operators ====
 
   
The list of predefined operators includes stardard arithmetic operators:
 
  +
The format always should begin with <code>%</code> and end with one of <code>f</code>, <code>e</code>, or <code>g</code>, possibly followed by <code>#</code>. Here, <code>f</code> means fixed-point notation (e.g. <code>452.116</code>), <code>e</code> means exponential notation (e.g, <code>3.578E-5</code>), and <code>g</code> means use the form most appropriate for the magnitude of the number. Between the <code>%</code> and the letter you can (optionally) include <code>.<i>n</i></code> where <code><i>n</i></code> is the number of decimal digits to use for the number. If the format ends in <code>#</code>, then trailing zeros are removed after the number is formatted. (More sophisticated formats are possible, but this describes the basics.)
   
  +
Context()->{format}{number} = "%.2f"; # format numbers using 2-place decimals (e.g., for currency values).
  +
Context()->{format}{number} = "%.4f#"; # format numbers using 4-place decimals, but remove trailing zeros, if any.
   
* / + - ! >< U ^ **
 
  +
The default format is <code>"%g"</code>.
. ,
 
   
  +
The Context also includes information about what should count as a number when an answer is parsed. There are two patterns for this, a signed number and an unsigned number. The latter is what is used in parsing numbers (and the sign is treated as unary minus); former is used in the <code>[[Common MathObject Methods#Value::matchNumber|Value::matchNumber()]]</code> function. These are stored in the <code>Context()->{pattern}</code> hash; the default values are:
   
Where ! is the factorial operator, both ^ and ** give exponentiation, . is a dot product, and , is a list operation used to create lists.
 
  +
Context()->{pattern}{number} = '(?:\d+(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
  +
Context()->{pattern}{signedNumber} = '[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
   
You may also find the operators u+, u-, and fn, but these are internal operators and should not be modified.
 
  +
These are fairly complicated regular expressions that match the usual fixe-point and exponential notation for numbers in WeBWorK. It is possible to change these patterns to handle things like commas instead of decimals for European usage, or to allow commas every three digits. Note, however, that you would need to include a <code>[[Context flags#NumberCheck|NumberCheck]]</code> routine that would translate the special format into the required internal format. For example, this allows you to enter numbers as hexadecimal values:
   
Here you will most often want to undefine some operators. The following example undefines cross product and dot product:
 
  +
#
  +
# Numbers in hexadecimal
  +
#
  +
Context()->{pattern}{number} = '[0-9A-F]+';
  +
Context()->{pattern}{signedNumber = '[-+]?[0-9A-F]+';
  +
Context()->flags->set(NumberCheck => sub {
  +
my $self = shift; # the Number object
  +
$self->{value} = hex($self->{value_string}); # convert hex to decimal via perl hex() function
  +
$self->{isOne} = ($self->{value} == 1); # set marker indicating if the value is 1
  +
$self->{isZero} = ($self->{value} == 0); # set marker indicating if the value is 0
  +
});
  +
Context()->update;
   
  +
Note that after changing the <code>pattern</code> you must call <code>Context()->update</code> to remake the tokenization patterns used by the Context.
   
Context()->operators->undefine('><','.');
 
  +
Here is an example that lets you use commas in your numbers:
   
  +
#
  +
# Allow commas every three digits in numbers
  +
#
  +
Context()->{pattern}{number} = '(?:(?:\d{1,3}(?:\,\d{3})+|\d+)(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
  +
Context()->{pattern}{signedNumber} = '[-+]?(?:(?:\d{1,3}(?:\,\d{3})+|\d+)(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
  +
Context()->flags->set(NumberCheck => sub {
  +
my $self = shift; # the Number object
  +
my $value = $self->{value_string}; # the original string
  +
$value =~ s/,//g; # remove commas
  +
$self->{value} = $value + 0; # make sure it is converted to a number
  +
$self->{isOne} = ($self->{value} == 1); # set marker indicating if the value is 1
  +
$self->{isZero} = ($self->{value} == 0); # set marker indicating if the value is 0
  +
});
  +
Context()->update;
   
You can also redefine an operator which was previously undefined.
 
  +
If you want to make the numbers display with commas, then you will need to subclass the <code>Value::Real</code> object and override the <code>string()</code> and <code>TeX()</code> methods to insert the commas again, and then tie your new class into the <code>Context()->{value}{Real}</code> value. For example, in addition to the changes above, you might do
   
Context()->operators->redefine('><','.');
 
  +
#
  +
# Subclass the Value::Real class and override its string() and TeX()
  +
# methods to insert commas back into the output
  +
#
  +
package my::Real;
  +
our @ISA = ('Value::Real'); # subclass of this Value::Real
  +
  +
sub string {
  +
my $self = shift; my $x = $self->SUPER::string(@_); # get the original string output
  +
my ($n,@rest) = split(/([.E])/,$x,1); # break it into the integer part and the rest
  +
while ($n =~ m/[0-9]{4}(,|$)/) # add commas as needed
  +
{$n =~ s/([0-9])([0-9]{3})(,|$)/$1,$2$3/}
  +
return join("",$n,@rest); # return the final string
  +
}
  +
  +
sub TeX {
  +
my $self = shift;
  +
my $n = $self->SUPER::TeX(@_); # original TeX uses string(), so commas are already there
  +
$n =~ s/,/{,}/g; # just make sure they have the correct spacing
  +
return $n;
  +
}
  +
  +
package main; # end of package my::Real;
  +
  +
Context()->{value}{Real} = "my::Real"; # make the Context use my::Real rather then Value::Real
  +
Context()->{format}{number} = "%f#"; # format using "f" rather than "g", so no exponential notation
   
To get rid of an operator you could also use
 
  +
This could be put into a separate macro file that you could load into your problems whenever it is needed. See [[Creating Custom Contexts]] for details.
   
Context()->operators->remove('><','.');
 
  +
=== Lists and Delimiters ===
   
but this is not recommended, as undefine makes the operator unavailable (but still recognized), while after remove WebWork will not recognize the operator at all. Thus if an operator is undefined, students will get sensible error messages indicating that the operator is not available in the context of the problem.
 
  +
The Context object contains two more collections of data that were not mentioned in the [[Introduction to Contexts]]: the <code>lists</code> and <code>parens</code> objects. These are closely related, and determine what types of objects are created from various delimiters like braces and brackets. For example, in some contexts parentheses form Points, while in others they form Intervals or Lists. This is controlled by the settings in these two objects.
==== (2) Functions ====
 
   
The list of predefined functions is
 
  +
The <code>lists</code> object contains the definitions for the various types of list-like objects such as Points, Vectors, and Intervals. Each of the types of list has an entry that tells the parser what class implements the list, and specifies the open and close delimiters and the separators that will be used by default to display an instance of the class. A special case is <code>AbsoluteValue</code>, which is treated as a list since it has open and close delimiters, even though the list can only contain one element.
   
sin, cos, tan, sec, csc, cot, asin, acos, atan, asec,
 
  +
{| class="wikitable"
acsc, acot, sinh, cosh, tanh, sech, csch, coth, asinh,
 
  +
! List !! style="padding:5px" | Open !! style="padding:5px" | Close !! style="padding:5px" | Separator
acosh, atanh, asech, acsch, acoth, ln, log, log10, exp,
 
sqrt, abs, int, sgn, atan2, norm, unit, arg, mod,
 
Re, Im, conj
 
   
Here the most common need is to make some functions unavailable in the context of a problem. The following example makes functions needed only for complex variables unavailable:
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Point</code>
  +
| style="padding:5px; text-align:center" | <code>(</code>
  +
| style="padding:5px; text-align:center" | <code>)</code>
  +
| style="padding:5px; text-align:center" | <code>,</code>
   
Context()->functions->undefine('norm','unit','arg','mod','Re','Im','conj');
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Vector</code>
  +
| style="padding:5px; text-align:center" | <code>&lt;</code>
  +
| style="padding:5px; text-align:center" | <code>&gt;</code>
  +
| style="padding:5px; text-align:center" | <code>,</code>
   
You can also undefine entire collections of functions with disable, e.g.,
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Matrix</code>
  +
| style="padding:5px; text-align:center" | <code>[</code>
  +
| style="padding:5px; text-align:center" | <code>]</code>
  +
| style="padding:5px; text-align:center" | <code>,</code>
   
Context()->functions->disable("Trig");
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>List</code>
  +
| style="padding:5px; text-align:center" | <code></code>
  +
| style="padding:5px; text-align:center" | <code></code>
  +
| style="padding:5px; text-align:center" | <code>,</code>
   
The categories of functions are: SimpleTrig (with sin, cos, tan, sec, csc, and cot), InverseTrig (with asin, acos, atan, asec, acsc, acot and atan2), SimpleHyperbolic (with sinh, cosh, tanh, sech, csch, and coth), InverseHyperbolic (with asinh, acosh, atanh, asech, acsch, acoth), Numeric (with ln, log, log10, exp, sqrt, abs, int, sgn), Vector (with norm and unit) and Complex (with arg, mod, Re, Im, conj).
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Interval</code>
  +
| style="padding:5px; text-align:center" | <code>(</code>
  +
| style="padding:5px; text-align:center" | <code>)</code>
  +
| style="padding:5px; text-align:center" | <code>,</code>
   
There is also
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Set</code>
  +
| style="padding:5px; text-align:center" | <code>{</code>
  +
| style="padding:5px; text-align:center" | <code>}</code>
  +
| style="padding:5px; text-align:center" | <code>,</code>
   
Trig
 
  +
|- style="vertical-align:top"
(SimpleTrig together with InverseTrig), Hyperbolic (with SimpleHyperbolic and InverseHyperbolic), and All.
 
  +
| style="padding:5px" | <code>Union</code>
  +
| style="padding:5px; text-align:center" | <code></code>
  +
| style="padding:5px; text-align:center" | <code></code>
  +
| style="padding:5px; text-align:center" | <code>U</code>
   
==== (3) Constants ====
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>AbsoluteValue</code>
  +
| style="padding:5px; text-align:center" | <code>|</code>
  +
| style="padding:5px; text-align:center" | <code>|</code>
  +
| style="padding:5px; text-align:center" | <code></code>
   
The list of predefined constants is e, pi, i, j, k. The constant i denotes sqrt(-1) in Context("Complex"), denotes the vector <1,0> in Context("Vector2D"), and denotes the vector <1,0,0> in Context("Vector"), Context("Matrix"), and Context("Point"). The constant i is undefined outside of those contexts. The constants j and k are <0,1,0> and <0,0,1>, respectively, in Context("Vector") and Context("Matrix"). The constant j is <0,1> in Context("Vector2D"), and k is undefined there. The constants i, j and k are undefined outside of the contexts described above.
 
  +
|}
   
Context()->constants->set(i => {TeX=>'\boldsymbol{i}', perl=>'i'});
 
  +
The delimiters listed in this table are used when the object doesn't specify the delimiters explicitly via its <code>{open}</code> and <code>{close}</code> properties. This is usually the case when objects are created via the class constructors rather than parsing a string. For example, <code>Vector(4,0,-1)</code> would not have its <code>{open}</code> and <code>{close}</code> properties set, so would use the defaults in the <code>lists</code> object. Note that the Interval object has parentheses as its default delimiters, but the <code>Interval()</code> constructor will set the open and close properties automatically so that you can form open and closed intervals easily:
Context()->constants->remove("k");
 
Context()->constants->set(R => {TeX => '{\bf R}'});
 
   
  +
$I1 = Interval(1,2); # an open interval
  +
$I2 = Interval([1,2]); # a closed interval
  +
$I3 = Interval("(",1,2,"]"); # a half-open interval
   
==== (4) Variables ====
 
  +
It is also possible to put the delimiters at the end of the interval (which is how the Interval's <code>value()</code> method returns them): <code>Interval(0,1,"(","]")</code>.
   
The default context Context("Numeric") recognizes a single real variable x. You can set variables in your context like this:
 
  +
On the other hand, instances of these objects created by parsing a string usually save the open and closing delimiters in the object's <code>{open}</code> and <code>{close}</code> properties, so the default will not be used in those cases. E.g., <code>$v = Compute("<4,0,-1>")</code> would produce a Vector object with the <code>$v->{open} = "&lt;"</code> and <code>$v->{close} = "&gt;"</code>.
   
  +
To change the <code>list</code> settings, use the <code>set()</code> method, as usual:
   
Context()->variables->are(z=>'Complex');
+
Context()->lists->set(Vector => {open => "(", close => ")"});
Context()->variables->are(x=>'Real',y=>'Real',z=>'Real');
 
   
  +
Note that this only affects the case where the Vector object doesn't specify the open and close delimiters explicitly. It also doesn't change the delimiters that the parser uses to identify a vector, since the <code>list</code> values are only for output.
   
Note that declaring variables this way with 'are' indicates to the context that these are the only variables in the context. You can add variables to a context while preserving already defined variables by doing:
 
  +
To change what delimiter to use for a given object type, you need to change the <code>parens</code> object. This associates each open delimiter to one of the <code>list</code> types given above, and also gives the close delimiter that is needed to match it, and some other data about how it can be used. So to complete the change for Vectors, we would need to use
   
Context()->variables->add(x=>'Real',y=>'Real',z=>'Real');
+
Context()->parens->set("(" => {type => "Vector", close => ")"});
   
You can add any number of real or complex variables in this manner, just be careful that your variable names don't interfere with names of other defined objects. You may also create vector-valued variables, or variables of nearly any MathObject? type. For example:
 
  +
The other possible data for a <code>paren</code> object includes:
   
Context()->variables->set(r=>'Vector3D');
 
  +
{| class="wikitable"
Context()->variables->set(r=>Vector(1,2,3,4));
 
  +
! Name !! Description
   
You can set variable limits like this:
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px; white-space:nowrap" | <code>type => "<i>name</i>"</code>
  +
| style="padding:5px" | Specifies the list type that this open delimiter will genarate.
   
Context()->variables->set(x=>{limits=>[-1,1]});
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px; white-space:nowrap" | <code>close => "<i>c</i>"</code>
  +
| style="padding:5px" | The closing delimiter for this opening one.
   
The limits can also be set at the time the variable is created, as follows:
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px; white-space:nowrap" | <code>removable => 1</code> or <code>0</code>
  +
| style="padding:5px" | Do/don't remove delimiters when used around a single element. When <code>1</code>, don't create a list, just return the element.
   
Context()->variables->add(x => ~['Real',limits=>[-1,1]]);
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px; white-space:nowrap" | <code>formInterval => "<i>c</i>"</code>
  +
| style="padding:5px" | When present, this indicates that an Interval should be formed when the list is closed by the character <code><i>c</i></code> rather than the usual <code>close</code> character.
   
which creates a real variable x with limits from -1 to 1.
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px; white-space:nowrap" | <code>formList => 1</code> or <code>0</code>
  +
| style="padding:5px" | Do/don't allow this delimiter to form a List when the entries don't work for its <code>type</code>, otherwise produce an error.
   
==== (5) Strings ====
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px; white-space:nowrap" | <code>formMatrix => 1</code> or <code>0</code>
  +
| style="padding:5px" | Do/don't allow this delimiter to form a Matrix when the entries are appropriate for that.
   
The list of predefined strings is:
 
  +
|- style="vertical-align:top"
* infinity,
 
  +
| style="padding:5px; white-space:nowrap" | <code>emptyOK => 1</code> or <code>0</code>
* inf,
 
  +
| style="padding:5px" | Do/don't allow empty delimiters. When <code>0</code> there must be at least one element between the open and close delimiters.
* NONE,
 
* DNE.
 
   
The strings listed in a context indicate allowed responses by students even though
 
  +
|- style="vertical-align:top"
the responses might not be correct.
 
  +
| style="padding:5px; white-space:nowrap" | <code>function => 1</code> or <code>0</code>
If a string is in the context then entering the string will not trigger an error (even if it
 
  +
| style="padding:5px" | Do/don't allow this delimiter to form a function call when preceded by a function name.
is not the correct answer). However the student enters a string such as 'undefined' which is not
 
listed in the context then an error message: " 'undefined' is not defined in this context" is returned.
 
   
You can add strings to your context as follows:
 
  +
|}
   
The following command adds string 'True', and makes 'T' and alias for 'True'.
 
  +
=== More about Variables ===
   
Context()->strings->add(True=>{},T=>{alias=>'True'});
 
  +
The [[Introduction to Contexts#Variables|Introduction to Contexts]] shows how to add variables to a Context. One thing to keep in mind is that most Contexts come with some variables pre-defined, and when you add new ones, the originals are still available. If you wish to have only the variables that you define, then use <code>are()</code> rather than <code>add()</code> to add the variables. For example,
   
The following line defines the string 'Continuous' in the current context:
 
  +
Context("Numeric");
  +
Context()->variables->add(t => "Real");
   
Context()->strings("Continuous"=>{});
 
  +
would add a new real variable <math>t</math>, which would be in addition to the <math>x</math> that is already in the <code>Numeric</code> context, while
   
This sort of thing is useful for writing problems in which the student must enter text. Be sure here to provide sensible aliases for equivalent terms which students may use. Another good practice is to define strings for incorrect answers which students are likely to enter.
 
  +
Context("Numeric");
  +
Context()->variables->are(t => "Real");
   
Another option for use on strings allows you to specify whether or not WebWork should be sensitive to uppercase/lowercase letters. By default WebWork is case insensitive with respect to any string which it recognizes.
 
  +
would remove any pre-defined variables and leave you with only one variable, <math>t</math>.
   
To change this for a String, the caseSensitive flag must be specified when the String is added to the Context:
 
  +
=== More about Constants ===
   
Context()->strings->add(True=>{caseSensitive=>1});
 
  +
The [[Introduction to Contexts#Constants|Introduction to Contexts]] shows how to add constants to a Context. Usually, the output for a constant is its name, but you might want to specify a different value, particular for its <math>\rm\TeX</math> output. You can set the constant's <code>TeX</code>, <code>string</code>, and <code>perl</code> values to control the output in those formats. For example,
   
  +
Context("Complex");
  +
Context()->constants->set(i => {TeX=>'\boldsymbol{i}', perl=>'i'});
   
==== (6) Lists ====
 
  +
would indicate that <math>\rm\TeX</math> output should be <math>\boldsymbol{i}</math>, while its Perl form should be just <code>i</code>. Similarly,
   
This section and the next section 'Parens' are closely related.
 
  +
Context("Interval");
WebWork considers the following objects to be types of lists:
 
  +
Context()->constants->set(R => {TeX=>'\mathbb{R}'});
* Point,
 
* Vector,
 
* Matrix,
 
* List,
 
* Interval,
 
* Set,
 
* Union,
 
* AbsoluteValue.
 
 
The most common modification made to lists are to which type of parentheses is used to enclose them.
 
The purpose of the following description is meant to make you aware of how the various parenthesis types are used by default.
 
   
* Points by default look like e.g. (3,4).
 
  +
would set the <code>R</code> constant to produce <math>\mathbb{R}</math> rather than <math>{\bf R}</math> in <math>\rm\TeX</math> output.
* Vectors by default look like e.g., <3,4,5>.
 
* Matrix objects by default look like ~[[2,3],[2,3]].
 
* A list by default looks like 3, 4, 5. An interval by default looks like (0,9), 0,9), etc.
 
* A set by default looks like {3,4,5}.
 
* A union by default looks like (-infinity,0? U (5,7].
 
* Absolute value by default looks like |-5|.
 
   
Next we'll discuss how to modify the type of parentheses used with the various objects.
 
  +
=== Adding New Functions ===
   
==== (7) Parens ====
 
  +
The [[Introduction to Contexts#Functions|Introduction to Contexts]] includes some information about adding new functions that can be used in student Answers. One approach is given in <code>[http://webwork.maa.org/pod/pg/macros/parserFunction.html pg/macros/parserFunctions.pl]</code>, which implements an easy way to add functions to a Context using Formula objects. But if your function doesn't just compute the result of a Formula, then you would have to implement a Perl-based function. That process is described here.
   
WebWork recognizes the full range of parentheses types, as explained above (e.g., (, <, [, { ). But by default their meanings are dependent on the context. You can change how this works.
 
  +
To make a new function, you must create a subclass of the <code>Parser::Function</code> class or one of its subclasses. It is easiest to do the latter, if possible, since these already handle most of the details. The subclasses are the following:
   
For example, this command will cause Vector objects to look like (3,4,5) instead of <3,4,5>:
 
  +
{| class="wikitable"
  +
! Subclass !! Description
   
Context()->parens->set('('=>{type=>'Vector'});
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Parser::Function::numeric</code>
  +
| style="padding:5px" | Functions with one real input producing a real result, or one complex input producing a complex result. Output type is determined by input type. Setting <code>nocomplex=>1</code> means complex input not allowed.
   
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Parser::Function::numeric2</code>
  +
| style="padding:5px" | Functions with two real input returning a real result.
   
==== (8) Flags ====
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Parser::Function::complex</code>
  +
| style="padding:5px" | Functions with one complex (or real) input returning a real or complex result. Result is real unless <code>complex=>1</code> is set.
   
A discussion of the context flags can be found under [[ContextFlags]]. The point to emphasize here is that to change the context flags in your context do, e.g.,
 
  +
|- style="vertical-align:top"
  +
| style="padding:5px" | <code>Parser::Function::vector</code>
  +
| style="padding:5px" | Functions with one vector input producing a real or vector result. The result is real unless <code>vector=>1</code> is set. Should always set <code>vectorInput=>1</code>.
   
Context()->flags->set(
 
  +
|}
tolerance => 0.0001,
 
tolType => 'absolute',
 
);
 
   
There are other ways to manipulate the context as well. For example, some function calls change the context:
 
  +
To implement your function, make a package that is a subclass of one of these, and give it a subroutine that computes the function you are interested in. Note that the number and type of inputs will already be checked, so there is not need to check that in your subroutine. Give your subroutine the same name as the function. E.g.,
   
Parser::Number::NoDecimals(Context());
 
  +
package my::Function::numeric;
  +
our @ISA = ('Parser::Function::numeric'); # subclass of Parser::Function::numeric
  +
  +
my $log2 = CORE::log(2); # cached value of log(2)
  +
  +
sub log2 { # the routine for computing log base 2
  +
shift; my $x = shift;
  +
return CORE::log($x)/$log2;
  +
}
  +
  +
package main; # end my::Function::numeric
   
Also, some macro files provide methods for changing the context:
 
  +
Now we have to hook the new function into the Context:
   
loadMacros("contextLimitedPowers.pl");
 
  +
Context("Numeric");
Context()->operators->set(@LimitedPowers::OnlyIntegers);
 
  +
Context()->functions->add(
  +
log2 => {class => 'my::Function::numeric', TeX => '\log_2', nocomplex => 1},
  +
);
   
=== Part II. Create Your Own Context ===
 
  +
This sets the name of the function to <code>log2</code> in the student answer, and uses <code>my::Function::numeric</code> to implement it. The <math>\rm\TeX</math> output is given as <math>\log_2</math>, and we are not allowing complex inputs. Once this is done, you can use things like
   
Suppose that you have accumulated a nontrivial number of context modifications which you use frequently when writing problems. You can combine all of these modifications into a single file and define your own context.
 
  +
$r = Compute("(1/3)*log2(5)");
  +
...
  +
ANS($r->cmp);
   
First, what do you want to call your context? Let's say we will name it MyContext. Whatever you end up naming your context, it is recommended practice to begin your filename with 'context' and end with '.pl'. So your filename might be <code>contextMyContext.pl</code>. When we're done we'll put the file in the <code>/pg/macros/</code> sudbdirectory of the WebWork installation.
 
  +
and students can use <code>log2(5)</code> in their answers.
   
You will then call the file in the loadMacros statement along with the other macros you load. You should call your context file after the standard macro files are called. So, for example, to call <code>contextMyContext.pl</code> in loadMacros one might do:
 
  +
If you want to be able to use <code>log2()</code> directly in your Perl code, then you should also define
   
loadMacros(
 
  +
sub log2 {Parser::Function->call("log2",@_)}
"PGstandard.pl",
 
"MathObjects.pl",
 
"contextMyContext.pl",
 
"PGcourse.pl",
 
);
 
   
When defining a new context in your file, the code will proceed as follows:
 
  +
in the <code>main</code> package. Then you can do
(i) copy an existing context which is close to what you want
 
(ii) modify what you want changed.
 
   
For example, this line copies <code>Context("Numeric")</code> into a new <code>Context("MyContext")</code>:
 
  +
$r = (1/3)*log2(5);
   
$context{MyContext} = Parser::Context->getCopy(undef,"Numeric");
 
  +
directly (i.e., without <code>Compute()</code>).
   
The following lines modify some aspects of this context:
 
  +
It would be possible to put the required definitions into a macro file that you can load into a problem file whenever it is needed; see [[Creating Custom Contexts]] for details.
   
$context{MyContext}->functions->disable("Trig");
 
  +
If your function isn't one that can be obtained by subclassing one of the classes listed above, then you will have to subclass <code>Parser::Function</code> directly. In this case, you will have to create <code>_check</code>, <code>_eval</code> and <code>_call</code> methods in your class in addition to the subroutine implementing your function. You can model these after the ones in any of the subclasses listed above (which are in the <code>[https://github.com/openwebwork/pg/tree/master/lib/Parser/Function pg/lib/Parser/Function]</code> directory). Note that the <code>checkNumeric()</code> and other service routines are in <code>[https://github.com/openwebwork/pg/blob/master/lib/Parser/Function.pm pg/lib/Parser/Function.pm]</code>.
$context{MyContext}->parens->set('('=>{type=>'Vector'});
 
etc...
 
   
So your complete file will look like:
 
  +
=== Adding New Operators ===
   
loadMacros(
 
  +
The [[Introduction to Contexts#Operators|Introduction to Contexts]] gives information about controlling the operators that are part of the Context. To add a new operator you need to make a subclass of <code>Parser::BOP</code> or <code>Parser::UOP</code> depending on wether the new operator is a binary or unary operator. This should implement the <code>_check()</code> and <code>_eval()</code> methods to check that its operands are correct and to compute its value. It might also need to implement other methods like <code>TeX()</code> or <code>string()</code> or <code>perl()</code>. There are a number of examples in the <code>[https://github.com/openwebwork/pg/tree/master/lib/Parser/BOP pg/lib/Parser/BOP]</code> and <code>[https://github.com/openwebwork/pg/tree/master/lib/Parser/UOP pg/lib/Parser/UOP]</code> directories.
"PGstandard.pl",
 
"MathObjects.pl",
 
"contextMyContext.pl",
 
"PGcourse.pl",
 
);
 
 
$context{MyContext} = Parser::Context->!getCopy(undef,"Numeric");
 
$context{MyContext}->functions->disable("Trig");
 
$context{MyContext}->parens->set('('=>{type=>'Vector'});
 
   
That's it. Save it, put it in <code>/pg/macros/</code> or your course <code>templates/macros</code> directory, and load it in your problems with loadMacros().
 
  +
The example given below implements a binary operator that performs "<math>n</math> choose <math>r</math>" so that <code>n # r</code> means <math>n \choose r</math>.
   
Note that when it comes to answer checking (discussed elsewhere), context checked is the one where the object was created, not the currently active one (when they differ). This means you can create an object, change the context, then create another one in order to get answer checkers from two different contexts.
 
  +
#
  +
# A package for computing n choose r
  +
#
  +
package my::BOP::choose;
  +
our @ISA = ('Parser::BOP'); # subclass of Binary OPerator
  +
  +
#
  +
# Check that the operand types are numbers.
  +
#
  +
sub _check {
  +
my $self = shift; my $name = $self->{bop};
  +
return if $self->checkNumbers(); # method inherited from Parser::BOP
  +
$self->Error("Operands of '%s' must be Numbers",$name);
  +
}
  +
  +
#
  +
# Compute the value of n choose r.
  +
#
  +
sub _eval {
  +
shift; my ($n,$r) = @_; my $C = 1;
  +
$r = $n-$r if ($r > $n-$r); # find the smaller of the two
  +
for (1..$r) {$C = $C*($n-$_+1)/$_}
  +
return $C
  +
}
  +
  +
#
  +
# Non-standard TeX output
  +
#
  +
sub TeX {
  +
my $self = shift;
  +
return '{'.$self->{lop}->TeX.' \choose '.$self->{rop}->TeX.'}';
  +
}
  +
  +
#
  +
# Non-standard perl output
  +
#
  +
sub perl {
  +
my $self = shift;
  +
return '(my::BOP::choose->_eval('.$self->{lop}->perl.','.$self->{rop}->perl.'))';
  +
}
  +
  +
package main;
   
===See also===
 
  +
This defines the new operator, but now we need to add it into the Context.
   
[[IntroductionToContexts]]
 
  +
Context("Numeric");
  +
  +
$prec = Context()->operators->get('+')->{precedence} - .25;
  +
  +
Context()->operators->add(
  +
'#' => {
  +
class => 'my::BOP::choose',
  +
precedence => $prec, # just below addition
  +
associativity => 'left', # computed left to right
  +
type => 'bin', # binary operator
  +
string => ' # ', # output string for it (default is the operator name with no spaces)
  +
TeX => '\mathbin{\#}', # TeX version (overridden above, but just an example)
  +
}
  +
);
   
[[ModifyingContexts]]
 
  +
Now you can do things like
   
  +
$p = Compute("5 # 3");
  +
  +
and students can use <code>#</code> to perform the same operation in their answers.
  +
  +
You can combine all of this into a macro file for inclusion into any problem that needs it. See [[Creating Custom Contexts]] for more information.
  +
  +
== See also ==
  +
  +
* [[Introduction to Contexts]]
  +
* [[Creating Custom Contexts]]
  +
* [[Course-Wide Customizations]]
  +
<div style="height:.5em"></div>
  +
* [[Context flags]]
  +
* [[Reduction rules for MathObject Formulas]]
  +
* [[Context Operator Table]]
  +
* [[Common Contexts]]
  +
  +
<br>
  +
  +
[[Category:Contexts]]
 
[[Category:MathObjects]]
 
[[Category:MathObjects]]
  +
[[Category:AIMWeBWorK Working Groups]]

Latest revision as of 18:00, 7 April 2021

Advanced Context Modifications

The Introduction to Contexts describes how to make basic modifications to a Context's variables, constants, strings, flags, functions, operators, and reduction rules. Here we will describe more advanced modifications and techniques involving the Context.

Number Formats

Real numbers are stored using a format that retains about 16 or 17 significant digits, making computations very accurate in most situations. When a number is displayed, you probably don't want to see all 17 digits (that would make a vector in three-space take up around 35 characters, for example). To make answers easier to read, MathObjects usually display only 6 significant digits. You can change the format used, however, to suit your needs. The format is determined by the Context()->{format}{number}, which is a printf-style string indicating how real numbers should be formatted for display.

The format always should begin with % and end with one of f, e, or g, possibly followed by #. Here, f means fixed-point notation (e.g. 452.116), e means exponential notation (e.g, 3.578E-5), and g means use the form most appropriate for the magnitude of the number. Between the % and the letter you can (optionally) include .n where n is the number of decimal digits to use for the number. If the format ends in #, then trailing zeros are removed after the number is formatted. (More sophisticated formats are possible, but this describes the basics.)

   Context()->{format}{number} = "%.2f";    # format numbers using 2-place decimals (e.g., for currency values).
   Context()->{format}{number} = "%.4f#";   # format numbers using 4-place decimals, but remove trailing zeros, if any.

The default format is "%g".

The Context also includes information about what should count as a number when an answer is parsed. There are two patterns for this, a signed number and an unsigned number. The latter is what is used in parsing numbers (and the sign is treated as unary minus); former is used in the Value::matchNumber() function. These are stored in the Context()->{pattern} hash; the default values are:

     Context()->{pattern}{number} = '(?:\d+(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
     Context()->{pattern}{signedNumber} = '[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';

These are fairly complicated regular expressions that match the usual fixe-point and exponential notation for numbers in WeBWorK. It is possible to change these patterns to handle things like commas instead of decimals for European usage, or to allow commas every three digits. Note, however, that you would need to include a NumberCheck routine that would translate the special format into the required internal format. For example, this allows you to enter numbers as hexadecimal values:

   #
   #  Numbers in hexadecimal
   #
   Context()->{pattern}{number} = '[0-9A-F]+'; 
   Context()->{pattern}{signedNumber = '[-+]?[0-9A-F]+';
   Context()->flags->set(NumberCheck => sub {
     my $self = shift;                              # the Number object
     $self->{value} = hex($self->{value_string});   # convert hex to decimal via perl hex() function
     $self->{isOne} = ($self->{value} == 1);        # set marker indicating if the value is 1
     $self->{isZero} = ($self->{value} == 0);       # set marker indicating if the value is 0
   });
   Context()->update;

Note that after changing the pattern you must call Context()->update to remake the tokenization patterns used by the Context.

Here is an example that lets you use commas in your numbers:

   #
   # Allow commas every three digits in numbers
   #
   Context()->{pattern}{number} = '(?:(?:\d{1,3}(?:\,\d{3})+|\d+)(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
   Context()->{pattern}{signedNumber} = '[-+]?(?:(?:\d{1,3}(?:\,\d{3})+|\d+)(?:\.\d*)?|\.\d+)(?:E[-+]?\d+)?';
   Context()->flags->set(NumberCheck => sub {
     my $self = shift;                              # the Number object
     my $value = $self->{value_string};             # the original string
     $value =~ s/,//g;                              # remove commas
     $self->{value} = $value + 0;                   # make sure it is converted to a number
     $self->{isOne} = ($self->{value} == 1);        # set marker indicating if the value is 1
     $self->{isZero} = ($self->{value} == 0);       # set marker indicating if the value is 0
   });
   Context()->update;

If you want to make the numbers display with commas, then you will need to subclass the Value::Real object and override the string() and TeX() methods to insert the commas again, and then tie your new class into the Context()->{value}{Real} value. For example, in addition to the changes above, you might do

   #
   #  Subclass the Value::Real class and override its string() and TeX()
   #  methods to insert commas back into the output
   #
   package my::Real;
   our @ISA = ('Value::Real');    # subclass of this Value::Real
   
   sub string {
     my $self = shift; my $x = $self->SUPER::string(@_);  # get the original string output
     my ($n,@rest) = split(/([.E])/,$x,1);                # break it into the integer part and the rest
     while ($n =~ m/[0-9]{4}(,|$)/)                       # add commas as needed
       {$n =~ s/([0-9])([0-9]{3})(,|$)/$1,$2$3/}
     return join("",$n,@rest);                            # return the final string
   }
   
   sub TeX {
     my $self = shift;
     my $n = $self->SUPER::TeX(@_);     # original TeX uses string(), so commas are already there
     $n =~ s/,/{,}/g;                   # just make sure they have the correct spacing
     return $n;
   }
   
   package main;    # end of package my::Real;
   
   Context()->{value}{Real} = "my::Real";    # make the Context use my::Real rather then Value::Real
   Context()->{format}{number} = "%f#";      # format using "f" rather than "g", so no exponential notation

This could be put into a separate macro file that you could load into your problems whenever it is needed. See Creating Custom Contexts for details.

Lists and Delimiters

The Context object contains two more collections of data that were not mentioned in the Introduction to Contexts: the lists and parens objects. These are closely related, and determine what types of objects are created from various delimiters like braces and brackets. For example, in some contexts parentheses form Points, while in others they form Intervals or Lists. This is controlled by the settings in these two objects.

The lists object contains the definitions for the various types of list-like objects such as Points, Vectors, and Intervals. Each of the types of list has an entry that tells the parser what class implements the list, and specifies the open and close delimiters and the separators that will be used by default to display an instance of the class. A special case is AbsoluteValue, which is treated as a list since it has open and close delimiters, even though the list can only contain one element.

List Open Close Separator
Point ( ) ,
Vector < > ,
Matrix [ ] ,
List ,
Interval ( ) ,
Set { } ,
Union U
AbsoluteValue | |

The delimiters listed in this table are used when the object doesn't specify the delimiters explicitly via its {open} and {close} properties. This is usually the case when objects are created via the class constructors rather than parsing a string. For example, Vector(4,0,-1) would not have its {open} and {close} properties set, so would use the defaults in the lists object. Note that the Interval object has parentheses as its default delimiters, but the Interval() constructor will set the open and close properties automatically so that you can form open and closed intervals easily:

   $I1 = Interval(1,2);          # an open interval
   $I2 = Interval([1,2]);        # a closed interval
   $I3 = Interval("(",1,2,"]");  # a half-open interval

It is also possible to put the delimiters at the end of the interval (which is how the Interval's value() method returns them): Interval(0,1,"(","]").

On the other hand, instances of these objects created by parsing a string usually save the open and closing delimiters in the object's {open} and {close} properties, so the default will not be used in those cases. E.g., $v = Compute("<4,0,-1>") would produce a Vector object with the $v->{open} = "<" and $v->{close} = ">".

To change the list settings, use the set() method, as usual:

   Context()->lists->set(Vector => {open => "(", close => ")"});

Note that this only affects the case where the Vector object doesn't specify the open and close delimiters explicitly. It also doesn't change the delimiters that the parser uses to identify a vector, since the list values are only for output.

To change what delimiter to use for a given object type, you need to change the parens object. This associates each open delimiter to one of the list types given above, and also gives the close delimiter that is needed to match it, and some other data about how it can be used. So to complete the change for Vectors, we would need to use

   Context()->parens->set("(" => {type => "Vector", close => ")"});

The other possible data for a paren object includes:

Name Description
type => "name" Specifies the list type that this open delimiter will genarate.
close => "c" The closing delimiter for this opening one.
removable => 1 or 0 Do/don't remove delimiters when used around a single element. When 1, don't create a list, just return the element.
formInterval => "c" When present, this indicates that an Interval should be formed when the list is closed by the character c rather than the usual close character.
formList => 1 or 0 Do/don't allow this delimiter to form a List when the entries don't work for its type, otherwise produce an error.
formMatrix => 1 or 0 Do/don't allow this delimiter to form a Matrix when the entries are appropriate for that.
emptyOK => 1 or 0 Do/don't allow empty delimiters. When 0 there must be at least one element between the open and close delimiters.
function => 1 or 0 Do/don't allow this delimiter to form a function call when preceded by a function name.

More about Variables

The Introduction to Contexts shows how to add variables to a Context. One thing to keep in mind is that most Contexts come with some variables pre-defined, and when you add new ones, the originals are still available. If you wish to have only the variables that you define, then use are() rather than add() to add the variables. For example,

   Context("Numeric");
   Context()->variables->add(t => "Real");

would add a new real variable [math]t[/math], which would be in addition to the [math]x[/math] that is already in the Numeric context, while

   Context("Numeric");
   Context()->variables->are(t => "Real");

would remove any pre-defined variables and leave you with only one variable, [math]t[/math].

More about Constants

The Introduction to Contexts shows how to add constants to a Context. Usually, the output for a constant is its name, but you might want to specify a different value, particular for its [math]\rm\TeX[/math] output. You can set the constant's TeX, string, and perl values to control the output in those formats. For example,

   Context("Complex");
   Context()->constants->set(i => {TeX=>'\boldsymbol{i}', perl=>'i'});

would indicate that [math]\rm\TeX[/math] output should be [math]\boldsymbol{i}[/math], while its Perl form should be just i. Similarly,

   Context("Interval");
   Context()->constants->set(R => {TeX=>'\mathbb{R}'});

would set the R constant to produce [math]\mathbb{R}[/math] rather than [math]{\bf R}[/math] in [math]\rm\TeX[/math] output.

Adding New Functions

The Introduction to Contexts includes some information about adding new functions that can be used in student Answers. One approach is given in pg/macros/parserFunctions.pl, which implements an easy way to add functions to a Context using Formula objects. But if your function doesn't just compute the result of a Formula, then you would have to implement a Perl-based function. That process is described here.

To make a new function, you must create a subclass of the Parser::Function class or one of its subclasses. It is easiest to do the latter, if possible, since these already handle most of the details. The subclasses are the following:

Subclass Description
Parser::Function::numeric Functions with one real input producing a real result, or one complex input producing a complex result. Output type is determined by input type. Setting nocomplex=>1 means complex input not allowed.
Parser::Function::numeric2 Functions with two real input returning a real result.
Parser::Function::complex Functions with one complex (or real) input returning a real or complex result. Result is real unless complex=>1 is set.
Parser::Function::vector Functions with one vector input producing a real or vector result. The result is real unless vector=>1 is set. Should always set vectorInput=>1.

To implement your function, make a package that is a subclass of one of these, and give it a subroutine that computes the function you are interested in. Note that the number and type of inputs will already be checked, so there is not need to check that in your subroutine. Give your subroutine the same name as the function. E.g.,

   package my::Function::numeric;
   our @ISA = ('Parser::Function::numeric');     # subclass of Parser::Function::numeric
   
   my $log2 = CORE::log(2);                      # cached value of log(2)
   
   sub log2 {                                    # the routine for computing log base 2
     shift; my $x = shift;
     return CORE::log($x)/$log2;
   }
   
   package main;    #  end my::Function::numeric

Now we have to hook the new function into the Context:

   Context("Numeric");
   Context()->functions->add(
     log2 => {class => 'my::Function::numeric', TeX => '\log_2', nocomplex => 1},
   );

This sets the name of the function to log2 in the student answer, and uses my::Function::numeric to implement it. The [math]\rm\TeX[/math] output is given as [math]\log_2[/math], and we are not allowing complex inputs. Once this is done, you can use things like

   $r = Compute("(1/3)*log2(5)");
   ...
   ANS($r->cmp);

and students can use log2(5) in their answers.

If you want to be able to use log2() directly in your Perl code, then you should also define

   sub log2 {Parser::Function->call("log2",@_)}

in the main package. Then you can do

   $r = (1/3)*log2(5);

directly (i.e., without Compute()).

It would be possible to put the required definitions into a macro file that you can load into a problem file whenever it is needed; see Creating Custom Contexts for details.

If your function isn't one that can be obtained by subclassing one of the classes listed above, then you will have to subclass Parser::Function directly. In this case, you will have to create _check, _eval and _call methods in your class in addition to the subroutine implementing your function. You can model these after the ones in any of the subclasses listed above (which are in the pg/lib/Parser/Function directory). Note that the checkNumeric() and other service routines are in pg/lib/Parser/Function.pm.

Adding New Operators

The Introduction to Contexts gives information about controlling the operators that are part of the Context. To add a new operator you need to make a subclass of Parser::BOP or Parser::UOP depending on wether the new operator is a binary or unary operator. This should implement the _check() and _eval() methods to check that its operands are correct and to compute its value. It might also need to implement other methods like TeX() or string() or perl(). There are a number of examples in the pg/lib/Parser/BOP and pg/lib/Parser/UOP directories.

The example given below implements a binary operator that performs "[math]n[/math] choose [math]r[/math]" so that n # r means [math]n \choose r[/math].

   #
   #  A package for computing n choose r
   #
   package my::BOP::choose;
   our @ISA = ('Parser::BOP');            # subclass of Binary OPerator
   
   #
   #  Check that the operand types are numbers.
   #
   sub _check {
     my $self = shift; my $name = $self->{bop};
     return if $self->checkNumbers();             # method inherited from Parser::BOP
     $self->Error("Operands of '%s' must be Numbers",$name);
   }
   
   #
   #  Compute the value of n choose r.
   #
   sub _eval {
     shift; my ($n,$r) = @_; my $C = 1;
     $r = $n-$r if ($r > $n-$r);                  # find the smaller of the two
     for (1..$r) {$C = $C*($n-$_+1)/$_}
     return $C
   }
   
   #
   #  Non-standard TeX output
   #
   sub TeX {
     my $self = shift;
     return '{'.$self->{lop}->TeX.' \choose '.$self->{rop}->TeX.'}';
   }
   
   #
   #  Non-standard perl output
   #
   sub perl {
     my $self = shift;
     return '(my::BOP::choose->_eval('.$self->{lop}->perl.','.$self->{rop}->perl.'))';
   }
   
   package main;

This defines the new operator, but now we need to add it into the Context.

   Context("Numeric");
   
   $prec = Context()->operators->get('+')->{precedence} - .25;
   
   Context()->operators->add(
     '#' => {
        class => 'my::BOP::choose',
        precedence => $prec,         #  just below addition
        associativity => 'left',     #  computed left to right
        type => 'bin',               #  binary operator
        string => ' # ',             #  output string for it (default is the operator name with no spaces)
        TeX => '\mathbin{\#}',       #  TeX version (overridden above, but just an example)
     }
   );

Now you can do things like

   $p = Compute("5 # 3");

and students can use # to perform the same operation in their answers.

You can combine all of this into a macro file for inclusion into any problem that needs it. See Creating Custom Contexts for more information.

See also