I planning to author a set of WeBWork problems about permutations, but I am not sure on how to proceed. As an example, a typical question would be: Let a = (1 2 3)(4 5) and b = (2 4)(3 5) be permutations in S_5. Compute the product ba. Give the answer as a product of disjoint cycles. Acceptable answers should be: (1 4 3)(2 5) but also, for instance, (5 2)(1 4 3) or (5 2)(4 3 1). Preferably, the students should get warnings if they enter something which isn't a permutations, such as 5, or something wrongly formatted, like (143)(25) or something stupid like (e^(2*pi*i) 4 3)(2 5). I already have classes, written in perl, for doing basic things with permutations, such as multiplying, computing inverses and conjugacy classes and comparing, as well as parsing and formating permutations in various formats. What I am unsure about, is how I should interface this with WeBWork. Since I don't plan to spend to much time on this, I would, for now, be happy with a simple solution, which gives me the student input as a raw, unparsed string and let me mark the answer as correct or wrong and let me pass sensible error messages to the user. I am quite confident that I can do the parsing and checking myself. I couldn't find any documentation on what kind of object that was supposed to be passed to the ANS() function. I tried a hack, where I wrote a custom answer checker and plugged it into a numeric MathObject. Although I could get the student input as a raw string, which I could check manually, I didn't find a way to control what was shown in the "Entered", and "Answer Preview" fields. It seems like WeBWork insists on cooking the answer up for these fields. Although not a priority for the moment, it would also be nice to make the solution fit into WeBWork's architecture with Contexts and MathObjects. I don't have much experience with authoring WeBWork problems, but I browsed through the documentation and the code, and did some experimenting. I tried to model the permutation as a list of vectors (with the idéa to change to a new subclass "Cycle" of List if I got it to work). I was able to make WeBWork to write out a list of vectors on the format (1 2 3)(4 5), but I was not able to parse from that format. I managed to change the open and close symbols to parentheses, but I couldn't figure out how to change the separator from a comma to a space. Even if I got this to work, I don't consider the solution particularly good, since it would prevent us from dealing with lists of permutations. I would rather model it as a product of Cycles. I would be very grateful for any suggestion on how to solve this.
The ->value of a Permutation could be a perl array. For example Permutation("(1 2 3)(4 5)")->value could be something like (0, 1, 2, 3, 0, 4, 5,). (Or replace the 0 with some other marker.) I would think this would lead to the capacity for Lists. Or maybe the ->value would actually be subroutine code that executes the permutation on an input array reference.
Such an object might need an "n" property, which could be set as a context flag for typical Permutation objects or implied at the creation of each Permutation by the largest integer.
Checking of equality could be done by taking the larger "n" from each Permutation and just seeing what each does to each member of 1 through n. This would follow how Formula objects are compared. Actually, this is a bad idea in case a student enters something like "(143)". So instead, just check on any integer that is mentioned on either side of the equals sign. (That is, any integer from the ->value array.)
The Context could have a Limited version, where Permutations must be reduced (assessed by looking for repeated integers and presence of singleton cycles).
The hardest part might be making the parser check for a legitimate form. For example, disallowing "(1 (2 3)) (4 5)". But if you know what you are doing with regexp (which I definitely don't) then maybe that is not an obstacle.
You will probably want to remove all the usual binary operators, and only leave " " in place (which is usually implied multiplication). Take a look at contextPolynomialFactors.pl (a rather complicated context to my eyes, I'm afraid) to see how it checks that multiplication only takes place between a constant and a variable, or between two factors. That might be something you can modify to create a contextPermutation.pl.
Note that you might want to leave ^ as an operator. But you will have to supply a new definition.
The context could have a flag for whether you apply the permutations from the left or from the right.
The code
$ansEval = new AnswerEvaluator();
$ansEval->install_evaluator(sub {
$ans = shift;
$ans->{preview_latex_string} = "A text";
$ans->{correct_ans} = "Another text";
$ans->score(1);
return $ans;
});
ANS($ansEval);
seems to solve the problem of interfacing with the grading and answer displaying system. Of course, one would have to add code for actually checking the answers and printing sensible error messages.
I'm starting to feel that the whole abstract idéa behind MathObjects and Contexts is slightly off, but that might very well be because I don't understand them fully. It seems to me that it would be better if the author had to be more specific about what type of answer the student should give. For instance: A product of cycles (as in my problem), or a power-product of polynomials (as in the awkward code you referenced to), or a linear combination of vectors (the additive counter part), or a sum of positive integers (if you are asking for a partition of an integer). To me, that type is a property which belongs to each separate instance of an answer box, and not to the more diffuse concept of a Context.
Preferably, the parser should give back an object of the requested type. If you ask for a product of cycles, you should get back a product object, which contains the cycles. If you ask for a sum of integers, you should get back a sum object which contains the integers. Now it seems like the parser gives back an evaluation of the expression, which is sometimes, but far from always, what you want.
But now I'm getting seriously off topic. I should rather put some related questions in another thread.
Let me say a little about the purpose for the Context. In the earlier days of WeBWorK, each type of answer had its own answer checker that knew nothing about any other type of answer. So if you had a problem that had a function f(x) and you asked for two answers, its derivative, and that derivative evaluated at a particular value of x, your first answer would ask for a function and the second for a number. If the student misunderstood the idea and entered a function for the second one, he would get a message like "'x' has no meaning here." But of course, x has a meaning, and it is quite apparent to the student that it does, and so that message is unhelpful and doesn't tell the student what she needs to know in order to proceed.
The idea of the Context was that a problem has a context in which it operates (the variables, constants, etc., that are part of the problem), and all the answers in the problem should be able to make sense of those values and give appropriate responses to them. So in a MathObject version of the problem above, if the second answer included an x, the student would get a message like "Your answer isn't a number (it looks like a formula that returns a number)". Similar messages would be generated if they enter a list of numbers, or a point, or a vector, or whatever (if those are allowed in the problem).
The point is, an answer blank should know now only how to parse the correct answer, but also should understand the possible incorrect answers that a student might type, and should give consistent error messages no matter what type of answer was expected. That wasn't happening with the traditional answer checkers that only knew about one type of answer.
There are lots of other examples about how this was problematic.
When the MathObjects library was developed, the Context was developed as a means of allowing all the answer blanks to know about what was legal within the problem, and about how answers would be interpreted.
I'm not sure I understand your request. The author does have to be specific about the type of answer that the student should give. When the author does
ANS($answer->cmp);the type of
$answer
determines the type that the student will have to enter. The $answer
has a specific type, and the student's answer will have to be compatible with that. How the student's answer is parsed is controlled by the Context (but the context is the one associated with the $answer
), so the answer checker can recognize and respond to any type of answer the student enters (not just the type of $answer
).
Ideally, $answer
will have been created by parsing a string (just like the student answer is), so that you are sure the answer is possible in the context currently in use. That is why I suggest using Compute()
to create most answers rather than the constructor functions, though there are sometimes good reasons to use the latter.
So if you want to allow a product of cycles, you choose a context in which that is allowed, and then
$P = Compute("(1 3 2) (4 5)"); ... ANS($P->cmp);does mean you are indicating that the student's answer should be a product of cycles. How else is it that you intend that that should be done?
As for things like linear combinations of vectors, or sums of positive integers, these are possible to do, but in order to deal with them, you need some form of specification of what they are. Currently, there is no "linear combination of vectors" object, or "sum of positive integers" object, and in order to do as you ask, you would need to define such objects. There is certain more than one way that could be done, but providing a Context that knows how to parse them, and providing the Value object implementations that know how to compute with them is the way that the MathObjects library does that. Without some sort of object class that embodies the object you are interested in, I don't see how you are going to do it in any generality.
Even within the MathObject classes as they stand, you could use custom checkers to do what you want. For example, if you use
Context()->flags->set( reduceConstants => 0, reduceConstantFunctions => 0, );then, even if your answer is a constant in the end, you could walk the parse tree for the student's expression to see if it matches a particular structure. (The student's formula is in the
student_formula
field of the AnswerHash
that is the third parameter passed to your custom checker.)
For example, to do your sum of positive integers, you could walk the tree to check that each operator is a + and each operand is a positive integer. Your life can be made easier if you modify the context to remove all the operators other than + and modify the number pattern to only allow positive integers. Creating a MathObject class that codifies what a sum of positive integers really is would be even better.
Each answer box does have a type that it is looking for, but knows about the other types of things that a student might enter, and can parse those as well. The older answer checkers worked the way that you suggest, and that produced confusing results for students.
There is also the issue that some answer blanks should accept several different types without errors. For example, you might be asking for the limit of a function at some point, and the answer could be a number (a Real MathObject), plus or minus infinity (an Infinity MathObject), or a word like "DNE" for "does not exist" (a String MathObject). If the correct answer turns out to be a real number, but the real number checker doesn't know how to deal with infinity or words, then this leads to student frustration when they enter these answers and get confusing error messages (about "I" not being allowed, or "infinity" not having any meaning).
If authors have to include the strings that students might type, that leads to inconsistencies between problems ("inf" in one case and "infinity" in another), and the chance that they will not include the words in situations where the answer is just a real. The MathObject Numeric context knows about infinities and has the words "NONE" and "DNE" pre-defined, so they are always understood.
I'm not sure what quite what you mean by that. In particular, what "give back" refers to. When you use Compute()
, you will get an object of the type that your string represents. If you use one of the constructor functions, like Real()
you will get an object of the specified type (if that is possible). The exception is that Interval
might give you a Union if you did something like Interval("[0,1)U(3,4]")
(rather than producing an error).
If you mean that a custom checker may get something other than the type of the correct answer, that is true, but only to a limited extent. Your checker will only be called if the student answer parses to an object that is compatible with the correct one (where "compatible" is determined by its typeMatch
function). That means that if you have an answer that is a Real, you could get an infinity from the student, since those can be effectively compared by MathObject equality and inequality operations.
If you have something else in mind, I'm not sure what it is.
Again, I'm not sure what you are referring to. Can you be more specific about the situations you have in mind?
So although there are limitations to the context model, and it takes some work to specify new object types, it has proven to be flexible enough to handle a wide range of situations. By and large, it has been able to accommodate the people's needs. Of course, as you have found out, you do not have to use it, and can write an
AnswerEvaluator
of your own if you wish.
Thank to all your anwers and code examples I now understand much better how things works. You have also convinced me that it is relatively simple to extend the functionality of the MathObjects. At least if you first equip yourself with some knowledge.
I can see that many of my comments wasn't very clear. Probably because my understanding how things worked wasn't very clear. I will try to clarify some points.
I'm not sure I understand your request. The author does have to be specific about the type of answer that the student should give.
What I ment was that the construction
ANS($answer->cmp);
still only defines the "type" implicitly (here I really should have been more specific what I ment with "type"). Namely, it says that the answer should have the same type as the correct answer. But what is that? If I understand correctly, reals and integers are implemented by the same MathObject. Say that $answer contains a MathObject representing an integer. What kind of answers should we accept? I see at least three different interpretations:
- Any real number sufficently close to the correct answer.
- Only an integer which precisely matches the answer.
- Any constant expression that evaluates to the correct answer.
The first might be the right method if $answer just happens to be an integer. The second alternative is probably the right one in most basic combinatorial problems. You don't want the student to get a way with 10000 when the correct answer is 10001. In some situations you want the user to be able to type in 5! instead of 120, but certainly not when the question is: "What is 5 factorial?".
If I have understood things correctly, you can often fine tune such things by setting various flags on the context. But this might be easy to forget for the casual problem author. A more explicit way to solve this would be to demand the author to write something like:
ANS($answer, REAL); ANS($answer, INTEGER); ANS($answer, INTEGER_EXPRESSION);
This has the additional benefit of giving a cleaner cut between the value library and the parser library that you mentioned in another post. In fact, there might not even be a need for value objects to be of a specific class. This would facilitate code reuse by allowing authors to use code from libraries not originally developed for WeBWork.
Each answer box does have a type that it is looking for, but knows about the other types of things that a student might enter, and can parse those as well. The older answer checkers worked the way that you suggest, and that produced confusing results for students.
I totally agree that this is much better. But it should be possible to have this even if you require the author to be explict about the type in the sense described above.
- Any real number sufficiently close to the correct answer.
"sufficiently close" could mean in absolute terms, or relative terms. Either way, use
Context("LimitedNumeric"); #disallows any arithmetic operations and functions like sqrt() in answers
and either set
Context()->flags->set(tolType => absolute, tolerance => some number);
or
Context()->flags->set(tolType => relative, tolerance => some number);
The default for LimitedNumeric (I believe) has tolType => relative, tolerance => 0.001 - Only an integer which precisely matches the answer.
Same as above, except use
Parser::Number::NoDecimals();
after declaring the context to disallow decimals in answers (with appropriate feedback) and use absolute tolerance with the tolerance set to something like 0.5. - Any constant expression that evaluates to the correct answer.
Here, use
Context("Numeric");
and set the error tolerance flags as you would like them to be.
ANS($answer => cmp(cmp_class => 'an integer'));
This will make it so that if a student answer with something like "x^2" or "(1,2)" they will get feedback messages like
Your answer does not look like an integer. (It looks like a formula that returns a number.)
or
Your answer does not look like an integer. (It looks like an interval.)
Without specifying the cmp_class to "an integer", these messages would read:
Your answer does not look like a real number. (It looks like a formula that returns a number.)
or
Your answer does not look like a real number. (It looks like an interval.)
which could be misleading if the student really should know that an integer is expected.
Also, in the first and third situations from my previous post, there would be no feedback message for an answer like "1.2" other than to say it was incorrect.
In the second situation, an answer like "1.2" would lead to a message that is something like
You are not allowed to use decimals.
It's all these automated feedback messages that make using Math Objects so powerful. Once you know how to use them, you only need a few lines of code to activate a large bag of feedback messages tailored to the student's input.
One thing to remember is that originally WeBWorK was basically for calculus, and the distinction between integers and reals was not crucial to that need. This was still the case when MathObjects were developed, though there was a need at the tome to branch out to include things like intervals and vectors. This is what MathObjects was originally intended to provide.
The traditional answer checkers could do things like the distinction between a real constant and an expression resulting in a real by using different checkers (
strict_num_cmp()
versus std_num_cmp()
, for example), or through a parameter more like you are suggesting (num_cmp($x,mode=>"strict")
versus num_cmp($x)
). There were several other modes like frac
for allowing a fraction but no other operations, or arith
for allowing operators but not function calls.These work well for what they do, but they don't provide much flexibility to customize the actions they perform. For example, you may want to ask for
sin(pi/3)
in terms of square roots, so you need sort()
, so only the standard version would work; but you don't want to allow sin()
, so the standard version doesn't work. This lead to several hacks to provide this type of functionality, but they were not very satisfactory.In order to do what you ask, you would need to provide many distinct "types" that you could ask for. With the Context approach, you have individual control over each function, operator, constant, and so on, so you can get precisely the combination of features you need. The cost is that it can take a bit to configure the context to the way you want it to be. But we've tried to mitigate that by providing pre-defined contexts that are set up for specific needs, plus facilities for modifying them when needed.
For example, to get your factorial question, you could use
NoDecimals
in order to require integers, and disable the !
operator in order to avoid answers like 5!
. This would allow 5*4*3*2*1
as an accepted answer, but if you wanted to prevent that, you could start with the LimitedNumeric context, apply NoDecimals
and use absolute checks with a tolerance of .5 as Alex suggests. One could make these customizations and call the result Context("Integer")
and place it in contextInteger.pl
to share it.For the
sin(pi/3)
example, one could disable all the functions and then re-enable just sqrt()
. Use NoDecimals
to force only integers to be allowed to be entered, so that they can't enter a decimal approximation of the result. Then they could answer the question using sqrt(3)/2
, but not .866025
(though I guess they could enter 866025/1000000
). I don't really see how to accomplish this type of answer with your type-based approach without having to define a new type for this specific situation (something most authors aren't going to want to do).
As an aside, the way
ANS()
works, the first argument is the answer checker, and the remaining arguments are key-value pairs that are attached to the answer checker to provide options. So you would need to us something like ANS($answer,type=>"Real")
or whatever.Since the introduction of MathObjects, people have pushed the parser much further than its original design, in order to check answers that are of quite different types than the original calculus problems it was intended to handle. The idea of an integer class now makes more sense, and probably should be added.
->value
of a Permutation could be a perl array. For example Permutation("(1 2 3)(4 5)")->
value could be something like (0, 1, 2, 3, 0, 4, 5,)
. (Or replace the 0 with some other marker.) I would think this would lead to the capacity for Lists.
My implementation actually has two types: Permutation and Cycle, and the value()
of a Cycle returns an array like the one you describe, while for Permutation, it is an array of Cycles and Permutations used to create the Permutation. (It should probably return only Cycles, but right now it can include Permutations.)
->value
would actually be subroutine code that executes the permutation on an input array reference.You can apply a permutation to a number by multiplying on the left by the number (I only did left-to-right multiplication). So
loadMacros("contextPermutation.pl"); Context("Permutation"); $P = Compute("(1 3 2)(4 5)"); $m = 4 * $P;would set
$m
to 5. I didn't do mapping a permutation over an array, but you could do
@M = map {$_ * $P} (1,3,5);to obtain
@M = (3,2,4)
.
I would not recommend using that one as a starting point. It is too complex, and tries to do everything during the syntax checkin, which is probably the wrong approach. In any case, it does not make for a good example to learn from.
I have made a contextPermutation.pl
file (attached) that implements permutations and cycles as MathObjects. There are a number of options that allow you to control what the student is allowed to enter (e.g., are disjoint cycles required, are inverses allowed, etc.).
I understand your frustration in trying to develop this sort of thing in MathObjects yourself. There is not much documentation, and many of the existing context files are more complicated than you would want as an example of how to do this. The best information about MathObjects and Contexts is in the MathObjects section of the WeBWorK Wiki. The The Introduction to Contexts has some information about modifying contexts, while Modifying Contexts contains more about adding new classes, changing operators, add functions, and so on. Perhaps this new Permutation context can serve as a better example.
One complication about MathObjects is that it is split into two parts: the Value library and the Parser Library. The Value library is the computational part, which handles instances of the MathObject classes (like Real, Complex, Interval, etc), and provides the code that allows you to do things like add, multiply, compare, take roots of, print, etc. the various MathObject types. So things like
$z1 = Complex(3,-2); $z2 = sin($z**2 + 2*$z); print $z2->TeX,"\n";are handled by the Value library (the Parser is never involved). This library is pretty independent from WeBWorK itself, and could be used stand-alone without WeBWorK with very little change. (There are two modules that hook it into WeBWorK:
pg/lib/Value/AnswerCheckers.pm
and pg/lib/Value/WeBWorK.pm
).
The Parser library is what takes a string that represents an expression (e.g., one typed by a student) and turns it into a Value object of the appropriate type. The Compute()
and Formula
functions use this to produce MathObjects.
Historically, these two libraries were completely separate, but that distinction has become more hazy over time. For example, you can say something like $z = Complex("3-2i")
which causes the Value library to call the Parser library to parse the string "3-2i"
and returns the complex MathObject that it produces.
In any case, there are two distinct pieces that usually have to be dealt with when you want to work with an object type that isn't built into MathObjects. There is a Value-library part that implements the object type and the actions you can take with it (in this case, the Permutation and Cycle classes, and how to do things like multiply them, invert, them, etc), and a Parser-library part that tells WeBWorK how to build the new object from a string representation. In some sense, this is a separation of syntax (the Parser library) from the semantics (the Value library).
The Context object bridges the two libraries, and tells the Parser what Value objects to create. It knows what operators, variables, functions, constants, etc, are allowed, and what they mean. So to implement permutations and cycles, you would need to tell the Context that spaces are to be used between the numbers in a cycle, and between cycles to form permutations, and that parentheses around numbers form cycles.
You indicate that you were trying to use Lists of Vectors to simulate permutations, and that you could get them to output properly, but not parse properly. That is probably because you modified the portion of the Context that controls the Value objects and how they print, but not the Parser part. For example, you probably didn't modify the space operator to act as the comma usually does.
The attached contextPermutation.pl
does the things necessary to implement Permutation and Cycle objects in the Value library, and creates the operators and lists needed to properly create them using the Parser library. Note that this does allow you to produce lists of permutations. There is some redundancy, in that there need to be Cycle objects for both the Value and Parser libraries, and there are some checks (like type-checking for the entries) that are done in both places. I can say more about that if you want to know why that is the case. In practice, one could put most of the type-checking into the Value object, but doing it in the Parser makes it possible to do highlighting of the errant terms when error messages are generated.
I admit that producing such a context is not an easy thing to do, and there is not much in the way of documentation, so I certainly don't expect that you would have been able to come up with this myself.
I will say more about some of the other issues you raise in a separate response.
You have already found the AnswerEvaluator
object, which is at the heart of it. That is what you want to pass to ANS()
(though there are some other possibilities as well). The MathObject cup()
methods all return AnswerEvaluators
. The AnswerEvaluator
is passed an AnswerHash
object, which includes the information about what the student typed, so you could certainly use that to do the processing that you want. (I suspect you may already have done that.)
There is some documentation on AnswerHash
and AnswerEvaluator
, but it looks to be woefully out of date, and misformatted to boot.
Your code sample sets values for preview_latex_string
and correct_ans
, but you should also set student_ans
and correct_ans_latex_string
as well. The MathObject answer checkers do this for you automatically, but when you role your own by hand, you will have to handle that yourself.
If you wanted to use MathObjects (as an alternative to writing an AnswerEvaluator
by hand), you could use a custom answer checker with the ArbitraryString context (see the contextArbitraryString.pl
file). This passes the student input directly to your checker unprocessed. You could then parse and compare the answer in any way you wished, and could report errors via Value->Error()
. The MathObject code would already have taken care of setting the correct_ans
and other values for you (though you could override them if you wanted to format them better for your purposes), and you simply have to return the score for the problem.
One problem is that you shouldn't use the Numeric context to do this, because that will try to parse the answer (before you get it) using rules for producing a number. You will get error messages you don't want. That is why ArbitraryString is a better choice if you want to totally bypass the MathObject parsing.
As for the Entered and Answer Preview fields, these are the student_ans
(or the older preview_text_string
, but I think that was changed), and the preview_latex_string
fields of the AnswerHash
. For a custom checker for a MathObject, the AnswerHash
is the third parameter to the checker. You can replace the default values by your own, if you wish.
The WeBWork community is extremely kind and helpful! And I am amazed over the amount of work you have put into this, Davide.
Reading my original post again, I can see that it has an air of frustration. Eventually, I took a
pragmatic route to the problem, wrote a custom Permutation class, which I could write entirely
independently of WeBWork, and made a minimal ad hoc implementation of a parser and an AnswerEvaluator
that did what I needed but nothing more. I attach the code just in case someone finds any use of it.
I choose this approach for the following reasons:
* Although I am certainly not new to software engineering, I am new to both perl and programming
WeBWork exercises. Finding out how to solve something in perl is almost instant, given the huge
amount of information out there. Finding information on WeBWork is of course much harder, although it has
a very helpful community. It therefore made sense to do as much work as possible outside WeBWork.
* The code implementing MathObjects seems to be clean and well-designed and follows standard patterns for
parsers. But it is still complex enough to take some time to get acquainted with. And the documentation
about MathObjects in the wiki seems to be written mostly from a user perspective rather than a programmer perspective
(which is reasonable).
* Testing complex WeBWork problems through the web-interface is a real pain. Presumably, the right way to
go if one were to write a lot of exercises, would be to install a copy of WeBWork locally and edit the problems
directly in the file system. But setting this up could also take a lot of time...
Now the exercises I made are already in production. Given time, I might modify them to use your code instead and maybe
supplement your code to do things like generating random permutations and finding the order of a permutation.
I will get back and comment your other posts (and clarify some of my own comments).
directly in the file system. But setting this up could also take a lot of time...".
It sounds like you have made a good use of your time, and have hooked into WeBWorK in a reasonable way for your use. My code was to serve as an example of how to hook into MathObjects. You are absolutely correct that the documentation is focused on the problem author rather than those wishing to extend MathObjects in non-tricial ways. What little there is is in the links I have to the Introduction to Contexts and Modifying Contexts documents. These do give the basics for creating the classes needed and hooking them into the context; but it is pretty slim, for sure.
I agree with you that using the on-line editor is not well suited to large-scale work. Most of use do have WeBWorK installations on our laptops, and I work in the way you suggest. It can be a pain to set up WeBWorK, but there are a number of automated method at this point (like the one Arnie suggested), and so it is not so bad now as it once was.