Custom Answer Checkers

From WeBWorK_wiki
Jump to navigation Jump to search

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.

Note: When doing comparisons of functions with "==" you should put the correct answer on the left. See:

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.