As I mentioned in my comment to the question, I am quite fascinated by the idea of testing free-form input by numerical means similar to what the AcroTeX bundle has been able to do for a long time. This idea differs quite fundamentally from Vitaiy's suggestion, so I have tried to make it work in Mathematica.
I will suspend discussion of the crucial obstacle that the free CDF
format, though being the obvious choice to deploy such tests, does not currently allow InputField
s for strings. For the moment, I have to trust in the indirect promise made by Vitaliy that this will change in the future (I realize he doesn't speak for WRI).
Now it turns out that Mathematica has a function that is almost ideal for my suggested method: PossibleZeroQ. Implementing this as a test validator is the main idea of this answer.
The second idea is that with a tool as powerful as Mathematica, making a meaningful testing environment (even for harmless practice tests) requires us to shut off access to symbolic manipulations that can be used to cheat. This problem doesn't arise if we make tests where you only have to enter numbers, as in R.M.'s answer. The question goes much further than that, in that it wants us to validate student input in the form of functions (as in integrals and derivatives).
The main difference between my approach and Vitaliy's in this respect is that I implement a "white-list" instead of a "black-list" approach. That means I want allow only a certain class of functions to be entered. In particular, I will permit only those functions that have NumericFunction
in their Attribute
list.
The code below tries to implement these main ideas, putting them in the form of a basic dynamic grid where the questions appear next to answer fields. This requires some juggling with Hold
, HoldForm
, string conversion and replacements inside Dynamic
. Strings appear because for security reasons I accept the answer as String
input before converting it ton a held expression.
Here is the set of definitions:
Clear[check, quantQuestions, quantAnswers, prepareQuestions];
quantQuestions[
expressions__] := {ReleaseHold@Map[HoldForm, Hold[expressions]]};
SetAttributes[quantQuestions, HoldAll];
quantAnswers[questions__] :=
Map[ReleaseHold[(# /.
Integrate[f_, x_?AtomQ] :> integralWrapper[f, x]) /.
Integrate[f_, {x__}] :>
Integrate[f, {x}, GenerateConditions -> False]] &, questions];
SetAttributes[quantAnswers, HoldAll];
check[ans_, reference_] := Module[{ans1, ref1},
If[
Head[reference] === integralWrapper,
ans1 = D[ans, Last[reference]];
ref1 = First[reference],
ans1 = ans;
ref1 = reference
];
If[PossibleZeroQ[ans1 - ref1], 1, 2]]
check[ans_String, reference_] := 3;
stringCheck[
a_] := (Apply[And,
Map[MemberQ[Attributes[#], NumericFunction] &,
Cases[# /. Hold -> Unevaluated, x_[__] -> x, Infinity]]] /.
True :> ReleaseHold[#] /. False -> "no answer") &[
If[SyntaxQ[a], ToExpression[a, InputForm, Hold]]] /. Null -> ""
prepareQuestions[q_] :=
DynamicModule[{feedback = "Correct answers: ", maxAttempts = 3, ref,
ans, evaluation, attempts, active},
Check[ref = quantAnswers[q],
Print["Some questions cannot be answered"]; Abort[]];
ans = Map["" &, ref];
evaluation = Map[4 &, ref];
attempts = Map[0 &, ref];
active = Map[True &, ref];
Deploy@Labeled[
Framed[Grid[
Append[
Prepend[
Array[{
TraditionalForm[q[[#]]],
InputField[Dynamic[ans[[#]], Function[{a},
If[ans[[#]] != a, ans[[#]] = a;
If[! (active[[#]] = (attempts[[#]] += 1) <
maxAttempts),
FrontEndExecute[{FrontEnd`FrontEndToken[
FrontEnd`InputNotebook[], "MoveNextPlaceHolder"]}]];
];
evaluation[[#]] =
check[stringCheck[ans[[#]]], ref[[#]]]]],
String,
FieldSize -> 15,
Enabled -> Dynamic[active[[#]]]
],
Dynamic[{"Correct.", "Wrong.", "No answer",
""}[[evaluation[[#]]]]],
Row[{Dynamic[attempts[[#]]], " / ", maxAttempts}]} &,
Length[ref]
],
Map[Style[#, "Subsection"] &,
{"Question", "Answer", "Evaluation", "Attempts"}]
],
{InputField["", String,
FieldSize -> 1, ImageSize -> {0, 0},
Appearance -> "Frameless"]
}
]
],
FrameStyle -> Orange,
RoundingRadius -> 5
],
Row[{feedback, Dynamic[Count[evaluation, 1]]},
Background -> Orange, Frame -> True, FrameStyle -> None,
FrameMargins -> 5, RoundingRadius -> 5], Bottom,
BaseStyle -> "Subsubsection",
LabelStyle -> {"Subsection", LightOrange},
Background -> LightOrange]]
The starting point is quantQuestions
which wraps the set of questions in HoldForm
so they can be displayed as such. Internally, they are then unwrapped with quantAnswers
to get the simplified results with which to compare the user's input later.
The only other user-facing function is prepareQuestions
which is then applied like this:
prepareQuestions[
quantQuestions[
D[Sin[x + y], y],
D[Log[x], x],
Integrate[1/x^2, x],
Integrate[x, {x, 0, 1}]
]
]
This creates the dynamic input fields as shown here:
Here, I have already entered the answers and received the appropriate feedback.
You can try to enter the answers in different ways. Also try to enter cheats - they are supposed to be caught by the function stringCheck
. Its job is to comb the held form of the expression in the input string for non-numerical functions, of which D
and Integrate
are examples.
Edit
In the updated code above, I added a check for correct Mathematica syntax using SyntaxQ
. Other improvements are:
- The number of attempts at an answer is counted, and the input field is disabled after a maximum number (3 here).
- The total number of correct answers is displayed at the bottom.
- If a question has no answer, the error is displayed and no table is created.
- The problems are displayed using
TraditionalForm
(whether this is better is a matter of taste).
Edit 2
As pointed out in comments by Andrew and Murray, indefinite integrals have to admit constant offset. This can be done by comparing the derivatives, and I've implemented that for a single variable by substituting a formal wrapper (integralWrapper
) into the automatically computed answer (quantAnswers
) that tells check
to differentiate instead of integrate. It is only needed for indefinite integrals, and the rest of the validation (using PossibleZerQ
) works as before.
I'm also suppressing the generation of conditions for definite integrals, so that questions such as Integrate[x,{x,y,z}]
with unknown boundaries can be handled too.