Custom Answer Checkers
While the built-in MathObject answer checkers do a good job of testing a student response for agreement with a given correct answer, sometimes an answer may require a more sophisticated or customized check. For example, you might ask a student to provide a solution to a given implicit equation for which there are infinitely many solutions and would like to count any of them as correct. You can do this by providing your own answer checker as a parameter to a MathObject answer checker. This lets you get the advantage of the MathObject parsing, syntax checking, type checking, and error messages, while still giving you control over deciding when the student's answer is correct.
You do this by providing a checker
subroutine, as in the following example:
Context("Point"); $a = random(2,10,1); $x = random(-5,5,1); $y = $a - $x; BEGIN_TEXT Find a point \((x,y)\) that is a solution to \(x+y=$a\). $PAR \((x,y)\) = \{ans_rule(15)\} END_TEXT ANS(Point($x,$y)->cmp( showCoordinateHints => 0, # doesn't make sense to give hints in this case checker => sub { my ($correct,$student,$ansHash) = @_; # get correct and student MathObjects my ($sx,$sy) = $student->value; # get coordinates of student answer return ($sx + $sy == $a ? 1 : 0); # return 1 if correct, 0 otherwise } ));
The subroutine that is given as the value of the checker
parameter is called when the student has typed an answer that parses properly and is compatible with a point, so you don't have to worry about type-checking the student answer yourself, and are guaranteed to have a MathObject to work with. Note that your answer checker is tied to a given MathObject, so the type checking and error messages are appropriate for that type of object. Also, this is what will be shown as the correct answer if the student requests answers after the due date, so you should be sure that you provide an actual correct answer, even if you don't use $correct
within your checker.
Your checker subroutiune is passed three items, the correct answer (as a MathObject), the student's answer (as a MathObject), and a reference to the AnswerHash that is being used to process this answer. In the example above, $correct
will be the Point($x,$y)
that was used to create the original MathObject for the answer checker, and $student
will be the point that the student typed.
The answer hash ($ansHash
), holds additional data about the answer checker and the student's answer. That data includes all the flags passed to the answer checker; e.g., in the example above, $ansHash->{showCoordinateHints}
will be 0
, while $ansHash->{showTypeWarnings}
will be 1
(the default). Other fields that are useful include
Property | Description |
---|---|
$ansHash->{correct_ans}
|
The correct answer string |
$ansHash->{isPreview}
|
Whether the "Preview" button was pressed or not (you might want to limit error messages when this is true). |
$ansHash->{original_student_ans}
|
The unmodified string originally typed by the student. It has not been processed in any way. |
$ansHash->{student_formula}
|
The Formula obtained from parsing the student answer. If it is a constant-valued Formula, then $student is the result of evaluating this formula (i.e., it will be a Real or Point or some other MathObject rather than a Formula); if it is not constant, then $student will be this Formula.
|
$ansHash->{student_value}
|
The value passed to $student .
|
$ansHash->{correct_value}
|
The value passed to $correct .
|
The return value of your checker should be a value between 0 and 1 that indicates how much credit the student's answer should receive. Use 1 for full credit, .5 for 50% credit, and so on.
Error messages can be generated via the Value->Error()
function. For example,
Value->Error("Your value should be positive") if $student <= 0;
Note that this function will cause the checker to return with an error condition, so there is no need to return a score in this case.
Saving Custom Checkers
If you have written a custom checker that you want to use with more than one problem, or more than one answer in a single problem, then you might want to put that checker into a separate macro file that can be loaded into each problem that needs it. One way to do this is to make a variable that stores the code reference for the subroutine, and then pass that to the checker
option. For example,
$pointChecker = sub { my ($correct,$student,$ansHash) = @_; # get correct and student MathObjects my ($sx,$sy) = $student->value; # get coordinates of student answer return ($sx + $sy == $a ? 1 : 0); # return 1 of correct, 0 otherwise }; ANS(Point($x,$y)->cmp(showCoordinateHints => 0, checker => $pointChecker));
The $pointChecker
could be placed in a separate macro file placed in the course templates/macros
directory and loaded via loadMacros()
at the beginning of the problem along with the other macro files.
Note, however, that this subroutine relies on a constant, $a
, and it would be better to pass that to the checker. There are several ways to handle that. One would be to pass the value of $a
to the custom answer checker as an option to the cmp()
and retrieve it from the $ansHash
within the point checker subroutine. This is illustrated below.
$pointChecker = sub { my ($correct,$student,$ansHash) = @_; # get correct and student MathObjects my ($sx,$sy) = $student->value; # get coordinates of student answer my $a = $ansHash->{a}; # get the parameter value from cmp() call return ($sx + $sy == $a ? 1 : 0); # return 1 of correct, 0 otherwise }; ANS(Point($x,$y)->cmp(showCoordinateHints => 0, checker => $pointChecker, a => $a));
The other approach is to use a closure to create a subroutine that depends on an external value. In this case, we use a wrapper function that accepts the value of $a
as a parameter, and then returns the actual subroutine that is to be used as the checker
. This subroutine has access to the value of $a
that was passed to the outer function. Here is an example:
sub pointChecker { my $a = shift; # can be referenced by subroutine below return sub { my ($correct,$student,$ansHash) = @_; # get correct and student MathObjects my ($sx,$sy) = $student->value; # get coordinates of student answer return ($sx + $sy == $a ? 1 : 0); # return 1 of correct, 0 otherwise }; } ANS(Point($x,$y)->cmp(showCoordinateHints => 0, checker => pointChecker($a)));
A more sophisticated point checker could be passed the Formula to be evaluated as well as the value of $a
and the checker could use the Formula's eval()
method to get its value at the student's point and compare that to the value of $a
.