I want to make a multi choice question program in SML.
I have a text file whose content is structured as follows:
category1:Basic sml/sql
1-How many subsets does the power set of an empty set have?
A) One
B) Two
C) Three
D) Zero
Ans:A
2-What is the cardinality of the set of odd positive integers less than 10?
A) 20
B) 3
C) 5
D) 10
Ans:C
Each category has more than 5 questions
Each question belongs to a category
For each question there are 4 proposed answers followed by the answer on another line
I would like, to be able to retrieve and display to the user just the question (a random question) and the corresponding proposed answers. I have written this function that allows me to retrieve line by line all the content of my file. But I'm still stuck on how to unload a question-answer block.
fun getFromFile(file_name) =
let
val file = TextIO.openIn file_name
val text = TextIO.inputAll file
val _ = TextIO.closeIn file
in
String.tokens (fn c => c = #"\n") text
end
val table = getFromFile("question_file.txt");
How could I proceed? Is it possible to retrieve the lines of the file without passing them through a table first (retrieve the text directly)?
I'm still stuck on how to unload a question-answer block.
How could I proceed?
Find a way to encode multiple categories that contain multiple questions that contain multiple answers each. And once you have found a way to store that to a file (a file format), write a decoder. Your current decoder is a line decoder. You can encode things within things within things using lines, but you can also do it other ways.
For example, using JSON:
[
{
"category": "Basic sml/sql",
"questions": [
{
"question": "How many subsets does the power set of an empty set have?",
"answers": [
{ "answer": "Zero", "correct": false },
{ "answer": "One", "correct": true },
{ "answer": "Two", "correct": false },
{ "answer": "Three", "correct": false }
]
},
...
]
},
...
]
If relying on third-party libraries seems too difficult, you could come up with a file format yourself, e.g. a line-based one:
CATEGORY Basic sml/sql
QUESTION How many subsets does the power set of an empty set have?
ANSWER Zero
ANSWER_CORRECT One
ANSWER Two
ANSWER Three
QUESTION ...
So given your line-based reader, loop over each line and look at the first word:
CATEGORY
, start a new category.QUESTION
, start a new question within the current category.ANSWER
or ANSWER_CORRECT
, provide an option in the current question within the current category.This suggests a recursive function (since it needs to go over each line) that takes a number of parameters: The current category, the current question, and the total set of categories, questions and answers so far.
At this point you probably have to think: How do I store categories of questions with multiple answers in memory? What data type should I be using? E.g. using type aliases, you could express your data model like this:
type answer_option = string * bool
val example_answer_option = ("Zero", false) : answer_option
type question_answers = string * answer_option list
val example_question_answers =
("How many subsets does the power set of an empty set have?",
[
("Zero", false),
("One", true),
("Two", false),
("Three", false)
]
) : question_answers
type category = string * question_answers list
val example_category =
("Basic sml/sql",
[ example_question_answers ]
) : category
val example_categories = [ example_category ] : category list
The way SML type aliases work is that you get all of those expanded into the primitive types they consist of, so they may show up in your REPL as such:
> type answer_option = string * bool
type question_answers = string * (string * bool) list
type category = string * (string * (string * bool) list) list
which is considerably less readable and is one reason to use alternatives like datatype
, abstype
or opaque modules.
Going with this, however, you may define a stub like:
fun parse (line::lines, currentQuestion, currentAnswers, currentCategory, acc) =
case splitFirstWord line of
("CATEGORY", cat) => ...
| ("QUESTION", q) => ...
| ("ANSWER", aWrong) => ...
| ("ANSWER_CORRECT", aRight) => ...
| _ => raise Fail ("Unknown: " ^ line)
Now there are two sub-problems:
splitFirstWord
doesn't actually exist (yet).
There is a whole lot of book-keeping of current state.
Good luck!
Is it possible to retrieve the lines of the file without passing them through a table first (retrieve the text directly)?
I don't really understand this question. Unarguably, yes?
If by "table" you mean some kind of indexable data structure like a list:
Just don't call String.tokens (fn c => ...)
on the input.
This gives you a basic string
.
Note that table
is just the name of a value binding.
If you like, you can pass it through a chair
instead:
fun getFromFile(file_name) =
let
val file = TextIO.openIn file_name
val text = TextIO.inputAll file
val _ = TextIO.closeIn file
in
text
end
val chair = getFromFile "question_file.txt"
Note also that the parenthesis around function arguments is not necessary in SML. In fact, if you think they are, you'll probably make a syntax mistake soon enough. Try to avoid redundant syntax for greater clarity.