Custom Answer Checkers for Lists

From WeBWorK
Jump to: navigation, search

The built-in answer checkers for the List, Union, and Set classes are a bit more complicated than those for the other classes, because the order of elements in the List (or Intervals in a Union, or elements of a Set) usually doesn't matter. If you provide a checker routine for one of these types of MathObjects, it does not refer to the list as a whole, but to the individual elements in the list. Management of the list and determining how many elements are matched, what the partial credit should be, and what error messages to produce is all handled by the main list answer checker. You only provide a routine that determines when an entry in the student's list matches one in the correct list. Your checker will be called repeatedly on the various combinations of student and correct answers to determine if the student list matches the correct list (regardless of order).

When the checker is called from the list checker, it has two additional paramters:

   checker => sub {
     my ($correct,$student,$ansHash,$nth,$value) = @_;
     ...
   }

Here $nth is a word indicating which student answer is being tested (e.g., for the third element in the student's list, $nth will be " third" (note the leading space). This can be used in error messages, for example, to help the student identify where an error occurred. The $value is the name of the type of object that is in the list, e.g., "point". So you could use "Your$nth $value is not correct" as an error message in order to get something like "Your third point is incorrect" as the output.

As usual, you can use Value->Error() to generate an error message. The list checker will trap it and put it in the message area preceded by "There is a problem with your nth answer:" (where "nth" is replaced by the proper word for the answer that produced the error message). If you want to produce a message that doesn't include this prefix, use

   $correct->{context}->setError("message","",undef,undef,$Value::CMP_WARNING);
   return 0;

where "message" is the error message you want to produce.

If you want to act on the List (or Union or Set) as a whole, then you need to use the list_checker parameter instead. This specifies a subtroutine that will handle checking of the entire list, overriding the default list checking. That means you will be responsible for dealing with the possibly different order of the student answers from the correct answer, providing error messages about individual entries in the list, and so on.

The list_checker gets called with four parameters: a reference to an array containing the list of correct answers, a reference to an array of the student answers, a reference to the AnswerHash, and a string containing the name of the type of elements expected (for use in error messages). So a list checker looks like

   list_checker => sub {
     my ($correct,$student,$ansHash,$value) = @_;
     ...
   }

You can refer to the individual entries in $correct or $student as $correct->[$i] or $student->[$i], where $i is an integer representing the position within the list (the first entry is numbered 0 not 1).

Note that in a list_checker for a List object, you will have to do type checking yourself to check if the types of the entries are correct, since a list can consist of any type of elements.

The list_checker should return an array consisting of the number of student entries that are correct followed by any error messages that should be displayed (each on a separate line in the messages section).

An Example List Checker

Here is an example of a list checker:

   Context("Point");
   
   BEGIN_TEXT
   Three distinct points \((x,y)\) that satisfy the equation \(x+y=5\) are:
   \{ans_rule(20)\}
   END_TEXT
   
   ANS(List("(0,5),(1,4),(2,3)")->cmp(list_checker => sub {
     my ($correct,$student,$ansHash,$value) = @_;
     my $n = scalar(@$student);  # number of student answers
     my $score = 0;              # number of correct student answers
     my @errors = ();            # stores error messages
     my $i, $j;                  # loop counters
   
     #
     #  Loop though the student answers
     ##
     for ($i = 0; $i < $n; $i++) {
       my $ith = Value::List->NameForNumber($i+1);
       my $p = $student->[$i];   # i-th student answer
       #
       #  Check that the student's answer is a point
       #
       if ($p->type ne "Point") {
          push(@errors,"Your $ith entry is not a point");
          next;
       }
       #
       #  Check that the point hasn't been given before
       #
       for ($j = 0, $used = 0; $j < $i; $j++) {
         if ($student->[$j]->type eq "Point" && $student->[$j] == $p) {
           push(@errors,"Your $ith point is the same as a previous one") unless $ansHash->{isPreview};
           $used = 1; last;
         }
       }
       #
       #  If not already used, check that it satisfies the equation
       #    and increase the score if so.
       #
       if (!$used) {
         my ($a,$b) = $p->value;
         if ($a + $b == 5) {$score++} else {
           push(@errors,"Your $ith point is not correct") unless $ansHash->{isPreview}
         }
       }
     }
     #
     #  Check that there are the right number of answers
     #
     if (!$ansHash->{isPreview}) {
       push(@errors,"You need to provide more points") if $i < 3;
       push(@errors,"You have given too many points") if $score > 3 && $i != $score;
     }
     return ($score,@errors);
   }));

As you can see, list checkers are more complicated than checkers for the other types of data. But for certain situations like the one above, they can be indispensable.

Set and Union Answer Checkers

Since Sets and Unions are unordered lists of Reals or Intervals, they act a lot like lists. If you provide a checker for one of these, it works like the List checker: it is applied to the individual entries in the Set or the individual intervals in a Union. You may want to write a checker that works with the entire Set or Union at once, however. In that case, you need to use a list_checker in which you obtain the full Set or Union from $ansHash. For example:

   Context("Interval");
   
   BEGIN_TEXT
   Find a union of disjoint intervals that properly contains
   the numbers 0 and 5:
   \{ans_rule(20)\}
   END_TEXT
   
   ANS(Union("(-1,1) U (4,infinity)")->cmp(list_checker => sub {
     my ($correct,$student,$ansHash) = @_;
     my $U = $ansHash->{student_value}; # the complete student answer as MathObject
     if ($U->type ne "Union") {return (0,"Your answer should be a union of intervals")}
     my $n = scalar(@$student); # number of intervals
     my $S = Set(0,5);
     return ($U->contains($S) && $U != $S ? $n : 0);  # number of intervals correct
   }));

Note that the return value is the score followed by any error messages, and that the score is the number of intervals in the student's answer that are correct, which is why this checker returns $n when the answer is correct.

More error checking might be desired, here. For example, since a Union can be of Intervals or Sets, we might want to check that all the student's entries in the Union are actually intervals (the current version allows Sets). Note thta because the studentsMustReduceUnions parameter is 1 by default, we don't have to check for disjoint intervals. On the other hand, that flag also requires intervals to be merged into one if they could be, e.g., (0,1] U (1,2). We might want to work harder to allow that, since it is a disjoint union. We could also give more error messages to help the student work through a wrong answer. For example, we could check which point isn't covered and report that.


follow us