How can i get a random line from a text file in sml ? How to make a multi choice question program in sml?

126 Views Asked by At

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)?

1

There are 1 best solutions below

0
On BEST ANSWER

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:

  1. If it's CATEGORY, start a new category.
  2. If it's QUESTION, start a new question within the current category.
  3. If it's 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:

  1. splitFirstWord doesn't actually exist (yet).

  2. 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.