Bob:
Thanks for posting your generic_cmp code. I agree with you that the
"plug-in" approach is a good one, and didn't mean to suggest that you
shouldn't continue to work on your answer checker.
Looking through the code for your generic_cmp, things looked pretty
good to me (I recognized some of the code), but there were a few things
I was going to point out to you. For example, you really don't need to
pass the type of the correct answer, since you can get that from the
parsed object itself (for example, using the ->class method). Also,
you might want to check if the student is previewing rather than
submitting answers before you give an error message that tells the
student something about whether his answer is correct or not (the one
for the type mismatch already does this).
Some technical things: the protectHTML routine is available within the
Value package as Value::protectHTML(string), so you call that rather
than repeating its definition yourself. Also, the stuff at the end of
the routine that handles the error message is also available from the
Value package as a method of any parser object. So in your code, you
could use $v->cmp_error($ans) to replace the stuff in the "else"
clause.
I was going to write up a modified version for you, but as I was doing
it, I realized that some things like the length check and the type
matching, and so on, are already being done by the parser's built-in
answer checker, and so I thought that perhaps it would be better to
subclass that and replace the part that does the equality check. So I
wrote that instead.
It worked, but I was not satisfied with the number of routines that had
to be overridden in order to get at what amounted to only one line of
code in the parser's cmp_equal method (the line that performs the
overloaded == operator to see of the professor's and student's values
are equal). I have wanted to replace that line in other circumstances,
so I realized that I really needed to make one more level of
modularization and pull that one line out into a separate method that
could be overridden by subclasses. Following Bob's lead, however, I
made the new method look for a user-supplied checker, and if there
wasn't one, use the == operator. This lets you override the standard
equality check without having to subclass anything, so it gives you the
functionality of Bob's generic_cmp directly within the parser's native
answer checker. E.g.: ANS(Vector(1,2,3)->cmp(checker=>sub { my ($correct,$student,$ans) = @_; return 0 if $correct->length != $student->length; return 0 if norm($student) == 0; # don't accept zero vector return ($correct . $student) == 0; }, showCoordinateHints => 0);
would do a check to see if the student's answer is perpendicular to the
given vector, rather than equal to it. Note that you can pass
additional options to the answer checker, like showCoordinateHints
above (we don't want these when the answer is wrong, since we can't
tell whether an individual coordinate is right or not). Any of the
standard options for the answer checker of the object class of the
professor's answer can be specified in this way.
If you want to try it, you will need to get the updated
pg/lib/Value/AnswerChecker.pm file (and restart the server). I also
added a new file in pg/macros called answserCustom.pl that implements a
simple shell around this new feature. So if you load that macro file,
you can do something like: ANS(custom_cmp("<1,2,3>",sub { my ($correct,$student,$ans) = @_; return 0 if norm($student) == 0; return ($correct . $student) == 0; }, showCoordinateHints => 0);
You can specify some additional parameters that control wether your
checker is called only when the two vectors are the same length (true
by default), and so on. See the comments in the answerCustom.pl file
for details.
If you specify a custom checker for a List object (or a Union object,
which is actually implemented as a list of intervals), then the checker
will be called on the individual elements of the list to decide if they
are equal. So you don't have to write the code to process unordered vs.
ordered lists, or work out partial credit, or report appropriate
messages. You can just concentrate on the checking of entries in the
list, and the parser answer checker does the rest.
If you want, however, you can specify your own list-based checker that
gets passed the two lists, rather than the individual elements of the
lists. This would give you the ability to handle the lists in any way
that you choose. You can do this by using the list_checker=>code
option to the List(...)->cmp method, or by loading answerCustom.pl
and calling custom_list_cmp(). Again, see the documentation in that
file for details.
One nice thing about the implementation in the parser is that it traps
errors in the user-supplied code and can report them in a better way.
This is quite difficult to do in a macro file, but is easy to do in a
preloaded module file. Your checker code can call Value::Error(message)
to generate an error (and then die); the error message will appear in
the Messages area at the top of the screen rather than cause a pink
screen of death.
Here is an example problem that uses a custom checker to ask for three
distinct points that lie on a given implicit curve. The answer checker
tests if the point actually IS a solution, and also checks if the
student has given that point already, and issues an error message if he
did.
DOCUMENT(); # This should be the first executable line in the problem.
###################################################################### # # Example of using custom_cmp to install custom answer # checkers for various object types. # ######################################################################
loadMacros( PG.pl, PGbasicmacros.pl, PGanswermacros.pl, "Parser.pl", "answerCustom.pl", );
TEXT(&beginproblem);
############################################## # # The setup #
Context("Vector")->variables->are(x=>'Real',y=>'Real'); Parser::BOP::equality::Allow; # allow in professor's formula
# # Student answers must satisfy this equation # $f = Formula("x^2 + y^2 = 1");
Context()->operators->remove('='); # don't allow in student answers
############################################## # # The problem text #
Context()->texStrings; BEGIN_TEXT Find three distinct solutions to the equation \($f\): \{ans_rule(30)\} END_TEXT Context()->normalStrings;
############################################## # # The answer
@student_points = (); # store points the student got right $F = $f->perlFunction; # a function to evaluate the formula
sub checkSolutions { my ($correct,$student,$ans) = @_; # # Check that the student point IS a solution. # return 0 unless &{$F}($student->value); # # Check that it hasn't already been used. # # foreach $p (@student_points) { Value::Error("You can't use the same point more than once") if $p == $student; } # # Save the student's point for future checks. # push(@student_points,$student); return 1; }
ANS(custom_cmp("(1,0),(0,1),(sqrt(2)/2,sqrt(2)/2)",~~&checkSolutions)); $showPartialCorrectAnswers = 1;
##############################################
ENDDOCUMENT(); # This should be the last executable line in the problem.
Hope that makes things easier for you.
Davide
<| Post or View Comments |>
|