WeBWorK Problems

custom checker for interval answers

custom checker for interval answers

by Patti Lamm -
Number of replies: 1
I'm posting the following for Chris Wingard, who is having difficulty finding a way to register for the forums. (How is that being done these days?)

I have a question that asks for the domain of something like 16/(x^2 + 3x - 4). The answer is (-inf, -4) U (-4, 1) U (1, inf). A common mistake for our students is to leave out the middle interval and give something like (-inf, -4) U (1, inf). I'd like to make a custom error message to be displayed if the student makes such a mistake, but I'm having trouble getting the custom answer checker to work nicely with intervals. My first thought was to try something like

ANS( $dom -> cmp( checker => sub {
my ($correct, $student, $ans) = @_;
if ($student == $dom_bad)
{
Value::Error("Hint: Yes, x can be less than $b and " .
"greater than $c, but what about numbers " .
"between $b and $c?");
return 0;
}
return ($student == $correct);
})->withPreFilter('erase')->withPostFilter('erase'));

where $dom and $dom_bad were previously defined as

$dom = Interval("(-inf, $b) U ($b, $c) U ($c, inf)");
$dom_bad = Interval("(-inf, $b) U ($c, inf)");

but this, and variations on this, including changing between Numeric and Interval contexts, using interval_cmp vs. cmp, and using different parts of the answer hash all result in no messages being displayed. Any suggestions? Thanks.
In reply to Patti Lamm

Re: custom checker for interval answers

by Davide Cervone -
First, I would recommend that you use the answerHints.pl macros to make this easier. They are designed for exactly this situation (where you want to display a message for a particular wrong answer). Here is a snippet that does that:
    loadMacros("answerHints.pl");
    ...
    Context("Interval");
    ...
    $dom = Compute("(-inf, $b) U ($b, $c) U ($c, inf)");
    $dom_bad = Compute("(-inf, $b) U ($c, inf)");
    ...
    ANS($dom->cmp->withPostFilter(AnswerHints(
      $dom_bad => "Hint:  Yes, x can be less than $b or " .
                  "greater than $c, but what about numbers " .
                  "between $b and $c?"
    )));

It is also possible to make your original approach work, though it is harder. The one thing you need to know is that the domains you are using are not actually Interval objects, they are Union objects. (Note that the Interval() command will "upgrade" to a Union automatically, but I've changed it to Compute() above to avoid making it look like it is supposed to be an interval).

The reason this is important is because the Union object is a subclass of the List object (it is basically an unordered list of Intervals). That means the checker routine is called on the individual list elements (in this case the intervals) rather than on the list (or union) as a whole. So your equality is never true, and the message never generated.

To solve that, you would need to use a list_checker rather than a checker. This gets that array of intervals as the $correct and $student answers, rather than a MathObject itself, but you can use $ans->{correct_value} and $ans->{student_value} to get the MathObjects if you need them (as you do in your case).

So your modified version would be:

    ANS($dom->cmp(list_checker => sub {
      my ($correct, $student, $ans) = @_;
      $correct = $ans->{correct_value};
      $student = $ans->{student_value};
      if ($student == $dom_bad) {
        Value::Error("Hint: Yes, x can be less than $b and " .
        "greater than $c, but what about numbers " .
        "between $b and $c?");
      }
      return ($correct == $student);
    }));
Two things to note: First, I have changed the order of the final comparison to put the correct answer on the left. It doesn't really matter in this case, but would if they were functions, and so it is a good habit to get into. Second, I removed the unneeded return 0 after the error message is generated. Value::Error actually throws an error condition (which is trapped by the main answer checker), and that exits the routine, so the return statement is not needed.

Your original had the pre- and post-filters removed, but I'm not sure why you want that. If you need to do that for some reason, then you will need to be more careful in your custom checker, because you will have to deal with the possibility of there being NO answer provided (one of the pre-filters you removed is the blank answer checker). In that case, the modified routine is:

    ANS($dom->cmp(list_checker => sub {
      my ($correct, $student, $ans) = @_;
      return 0 unless $ans->{original_student_answer =~ /~~S/;
      $correct = $ans->{correct_value};
      $student = $ans->{student_value};
      if ($student == $dom_bad) {
        Value::Error("Hint: Yes, x can be less than $b and " .
        "greater than $c, but what about numbers " .
        "between $b and $c?");
      }
      return ($correct == $student);
    })->withPreFilter('erase')->withPostFilter('erase');
This checks to make sure the student answer contains some non-space character before proceeding. That is not necessary under normal circumstances, since the blank filter usually blocks empty answers from getting passed through, but removing the pre-filters means you have to work harder yourself.

Anyway, hope that does what you need.

Davide