This is a side discussion about the direction the book took (this is not an evaluation of your approach at all):
I know you are following a book there, but I am surprised at how the book approaches the solution. Using things like "if the pointers are null, the string is a name of an animal, … " seem very un-Ada like, so I am surprised the book went that route. If you are willing to entertain a different method on the side that doesn’t match the book exactly, here are my thoughts:
Since there are two distinct node types, I would have gone with an enumeration for the kinds of nodes and a discriminated record to handle each situation. Something like this:
-- Types for the overall design
type Node_Kind is (Question, Answer);
type Question_Result is (No, Yes);
-- Forward declaration and node access type
type Node;
type Node_Access is not null access Node;
-- Holds links to various question results
type Node_Links is array(Question_Result) of Node_Access;
-- Main node type
type Node(Kind : Node_Kind := Question) is record
case Kind is
when Question =>
Question : Unbounded_String;
Links : Node_Links;
when Answer =>
Answer : Unbounded_String;
end case;
end record;
The more interesting parts here:
- The record discriminant is defaulted. This allows me to use it in an array which is useful for holding a list of links to other nodes.
- I use an enumeration for the answer type. This will allow us to directly convert user input to an array index later. You can take the attribute
'Value
on the input string and if it matches an enumeration name (case insensitive), then it will convert it for you.
- My list of node links is indexed off the above enumeration, this can simplify selection logic later by using array indexing instead of IF/ELSE and CASE statements.
- I used Unbounded_String since the questions and answers can be any length
For the recursion, now your terminating condition is now if the discriminant is Answer
to indicate an answer node.
NOTE: The Node_Access
type declaration includes not null
which isn’t Ada95 conformant, but this can be removed if you want to either keep it Ada95 conformant.
Since I am using Unbounded_Strings, I generally like to have some helper functions to convert to/from strings and I find the To_String/To_Unbounded_String clutter up too much for me. If they don’t for you or if you just prefer to use them, then the next part is optional:
-- Utility conversion operations
function "+"(Value : String) return Unbounded_String
renames To_Unbounded_String;
function "+"(Value : Unbounded_String) return String
renames To_String;
That lets me do things like +"Convert this to Unbounded_String"
and vice versa. Again this is optional and too taste.
Now if you want to see my solution to the recursion problem you stated above, I’ll include it in a spoiler section below. It’ll be formulated around my changed interpretation of the problem:
function Recursive_Guess(Node : Node_Access) return String is
begin
-- Terminating condition for recursion
if Node.Kind = Answer then
return "My guess is " & (+Node.Answer);
end if;
-- ask question
Put(+Node.Question & " ");
-- loop until a valid answer (yes/no) is given
loop declare
-- Take the user input here at initialization
Answer_Str : constant String := Get_Line;
Answer : Question_Result;
begin
-- append newline to standard out after
-- the question is answered
New_Line;
-- Convert answer to enumeration. Invalid answers
-- are handled below in an exception handler
Answer := Question_Result'Value(Answer_Str);
-- Valid answer at this point so call this
-- function again recursively
return Recursive_Guess(Node.Links(Answer));
exception
when Constraint_Error =>
-- Wrong answer so put error message and loop
-- again for correct answer
Put("Invaild Answer, put yes or no: ");
end; end loop; -- end for declare block and loop
end Recursive_Guess;
The nice thing about this process is that the logic is:
- Read a line of text as the answer
- Convert the line to an enumeration value directly
- Select the node based on the enumeration value created
If the user puts in a non yes/no answer, then the exception handler fires, puts an error message and allows the code to loop to try for another answer.
NOTE: Get_Line
is not Ada95 conformant, but you can replace that call with the Get_Line
function example I talked about in the previous post.
Building a tree can be a bit more tedious when you use variant records, but I really like creating constructor functions for each situation to help make this less painful. For an Answer node, consider:
function New_Answer
(Answer_Str : String)
return Node_Access
is begin
return new Node'(Kind => Answer,
Answer => +Answer_Str);
end New_Answer;
For a new Question:
function New_Question
(Question_Str : String;
No_Node : Node_Access;
Yes_Node : Node_Access)
return Node_Access
is begin
return new Node'(Kind => Question,
Question => +Question_Str,
Links =>
(No => No_Node,
Yes => Yes_Node));
end New_Question;
That allows a pretty simple decision tree like this:
Root : constant Node_Access :=
New_Question("Is it a mammal?",
No_Node => New_Question("Is it a fish?",
No_Node => New_Answer("Beetle"),
Yes_Node => New_Answer("Trout")),
Yes_Node => New_Answer("Cow"));
Which can be more readable than trying to create the variant records in place.
The full code is below (in spoiler block), tested with the jdoodle online Ada compiler:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
procedure jdoodle is
-- Only needed for Ada_95
function Ada_95_Get_Line return String is
Buffer_Length : constant := 80;
Result : String(1..Buffer_Length);
Last : Natural := 0;
begin
Get_Line(Result, Last);
if Last = Buffer_Length then
return Result & Ada_95_Get_Line;
else
return Result(1..Last);
end if;
end Ada_95_Get_Line;
-- Types for the overall design
type Node_Kind is (Question, Answer);
type Question_Result is (No, Yes);
-- Forward declaration and node access type
type Node;
type Node_Access is not null access Node;
-- Holds links to various question results
type Node_Links is array(Question_Result) of Node_Access;
-- Main node type
type Node(Kind : Node_Kind := Question) is record
case Kind is
when Question =>
Question : Unbounded_String;
Links : Node_Links;
when Answer =>
Answer : Unbounded_String;
end case;
end record;
-- Utility conversion operations
function "+"(Value : String) return Unbounded_String
renames To_Unbounded_String;
function "+"(Value : Unbounded_String) return String
renames To_String;
-- Work horse function
function Recursive_Guess(Node : Node_Access) return String is
begin
-- Terminating condition for recursion
if Node.Kind = Answer then
return "My guess is " & (+Node.Answer);
end if;
-- ask question
Put(+Node.Question & " ");
-- loop until a valid answer (yes/no) is given
loop declare
-- Take the user input here at initialization
Answer_Str : constant String := Get_Line;
Answer : Question_Result;
begin
-- append newline to standard out after
-- the question is answered
New_Line;
-- Convert answer to enumeration. Invalid answers
-- are handled below in an exception handler
Answer := Question_Result'Value(Answer_Str);
-- Valid answer at this point so call this
-- function again recursively
return Recursive_Guess(Node.Links(Answer));
exception
when Constraint_Error =>
-- Wrong answer so put error message and loop
-- again for correct answer
Put("Invaild Answer, put yes or no: ");
end; end loop; -- end for declare block and loop
end Recursive_Guess;
-- Utility node construction functions
function New_Answer
(Answer_Str : String)
return Node_Access
is begin
return new Node'(Kind => Answer,
Answer => +Answer_Str);
end New_Answer;
function New_Question
(Question_Str : String;
No_Node : Node_Access;
Yes_Node : Node_Access)
return Node_Access
is begin
return new Node'(Kind => Question,
Question => +Question_Str,
Links =>
(No => No_Node,
Yes => Yes_Node));
end New_Question;
-- Test tree
Root : constant Node_Access :=
New_Question("Is it a mammal?",
No_Node => New_Question("Is it a fish?",
No_Node => New_Answer("Beetle"),
Yes_Node => New_Answer("Trout")),
Yes_Node => New_Answer("Cow"));
begin
Put_Line(Recursive_Guess(Root));
end jdoodle;