Logtalk and some Prolog systems provide a message printing mechanism that allows abstracting the message text, where the message is effectively printed, and how. Another key aspect of this mechanism is that a call to print a message can be intercepted by defining a hook predicate. Logtalk complements the message printing mechanism by providing also a question asking mechanism. Asking a question can also be intercepted by defining a hook predicate. This blog post illustrates the versatility of these hook predicates to abstract user interaction by using as example Douglas Adams ultimate question in his book “The Hitchhiker’s Guide to the Galaxy”. Jumping head first into the code:
:- category(hitchhikers_guide_to_the_galaxy).
:- multifile(logtalk::message_tokens//2).
:- dynamic(logtalk::message_tokens//2).
% abstract the question text using the atom ultimate_question
% the second argument, hitchhikers, is the application component
logtalk::message_tokens(ultimate_question, hitchhikers) -->
['The answer to the ultimate question of Life, The Universe and Everything is?'-[], nl].
:- multifile(logtalk::question_prompt_stream/4).
:- dynamic(logtalk::question_prompt_stream/4).
% the prompt is specified here instead of being part of the question text
% as it will be repeated if the answer doesn't satisfy the question closure
logtalk::question_prompt_stream(question, hitchhikers, '> ', user_input).
:- end_category.
The logtalk::message_tokens//2
non-terminal defines how to convert the abstract representation of the message
text (the atom ultimate_question
) into tokens. The
logtalk::question_prompt_stream/4
predicate defines a prompt and an input stream. Asking a question implies
printing a message (the question text after tokenization), and reading
a reply. After loading this category, we can ask the question using the logtalk::ask_question/5
predicate:
| ?- logtalk::ask_question(question, hitchhikers, ultimate_question, '=='(42), N).
The answer to the ultimate question of Life, The Universe and Everything is?
> 42.
N = 42
yes
Note the '=='(42)
argument, a closure that is used to check the user answer
to the question. The question is repeated until the goal constructed from that
that closure and the user answer succeeds. For example:
| ?- logtalk::ask_question(question, hitchhikers, ultimate_question, '=='(42), N).
The answer to the ultimate question of Life, The Universe and Everything is?
> icecream.
> tea.
> 42.
N = 42
yes
So far, we relied on the default behavior for the message printing and question asking mechanisms. All user interaction occurred at the top-level interpreter. This is fine when developing and manually testing. But deployed applications usually don’t include a top-level interpreter and testing is preferably automated. But how do we divert the question to e.g. a GUI interface and how do we automate testing of code where one of the steps is asking a user a question and waiting for its reply?
From top-level interpreter to GUI interface
We now want to divert the question and user answer to a GUI interface. For
example, a GUI dialog. This can be easily accomplished by using the
logtalk::question_hook/6
hook predicate. As there are no standard solutions for Prolog GUI interfaces
that we can use from Logtalk, we need to make a hard choice here. To continue
our example, we’re going to use a Java-based interface that you can play with
using JIProlog, SWI-Prolog, or YAP as the backend. An alternative would be to
use a web interface but that we would also restrict the compatible backends.
:- category(gui).
:- multifile(logtalk::question_hook/6).
:- dynamic(logtalk::question_hook/6).
:- meta_predicate(question_hook(*, *, *, *, 1, *)).
logtalk::question_hook(ultimate_question, _, hitchhikers, Tokens, Check, Answer) :-
tokens_to_text(Tokens, Question),
java('javax.swing.JFrame')::new(['The Hitchhiker''s Guide to the Galaxy'], Frame),
% repeat the question until we get the correct answer
repeat,
java('javax.swing.JOptionPane', Answer0)::showInputDialog(Frame, Question),
atom_codes(Answer0, Codes),
catch(number_codes(Answer, Codes), _, fail),
call(Check, Answer),
!,
java(Frame)::dispose.
% just a hack for this example
tokens_to_text([Question-[], _], Question).
:- end_category.
This category uses the Logtalk java
library that abstracts interfacing with Java. As we’re no longer relying on
default behavior, we use here a repeat loop to present the dialog to the
user until the correct answer is typed. By simply loading this category,
asking the question again will present the following dialog:
Note that this switch from a command-line interaction to a GUI interaction
doesn’t require any changes to the logtalk::ask_question/5
calls. In
actual applications, this means that we can switch the user interface
without any changes to the core application calls that print messages and
ask questions.
Automating user interaction for testing
To automate testing, we simply use the logtalk::question_hook/6
predicate to
provide a fixed answer. For example, we can provide the correct answer:
:- category(hitchhikers_fixed_answers).
:- multifile(logtalk::question_hook/6).
:- dynamic(logtalk::question_hook/6).
logtalk::question_hook(ultimate_question, question, hitchhikers, _, _, 42).
:- end_category.
In a practical case, the fixed answers would be used for followup goals being
tested. As mentioned before, the question answer read loop (which calls the
question check closure) is not used when a fixed answer is provided (using the
logtalk::question_hook/6
predicate) thus preventing the creation of endless
loops. With the hitchhikers_fixed_answers
category compiled and loaded, we
can now define a couple of tests for our example:
:- object(tests,
extends(lgtunit)).
test(questions_01, true(N == 42)) :-
logtalk::ask_question(question, hitchhikers, ultimate_question, '=='(42), N).
test(questions_02, true(N == 42)) :-
logtalk::ask_question(question, hitchhikers, ultimate_question, '=='(41), N).
:- end_object.
The tests can now run automated without any user interaction:
| ?- {tester}.
%
% tests started at 2019/11/14, 11:17:38
%
% running tests from object tests
% file: logtalk3/examples/questions/tests.lgt
%
% questions_01: success
% questions_02: success
%
% 2 tests: 0 skipped, 2 passed, 0 failed
% completed tests from object tests
%
% no code coverage information collected
% tests ended at 2019/11/14, 11:17:38
%
yes
Resources
This post is based on the questions
example distributed with Logtalk. A related example is
localizations
,
which illustrates how to use the message printing mechanism to provide
software localization in several natural languages.