home search about rss feed twitter ko-fi

App 2: Handling User Input (Scala 3 Video)

Now that you’ve seen that background on code blocks and recursion, it’s time to write a loop where we (a) prompt the user for their input, (b) read that input, (c) process it, and then prompt them again.

As you saw in the delete function I wrote in the database code, I used a for expression to work with Try values. If it’s been a while since you looked at that code, I recommend looking back at it now to see how it worked, because we’re about to do something similar.

Looping; input

Right now we have almost everything we need to prompt a user and read their input. We don’t have a way to process their input, but we can fake that for the time being. What I really want to do right now is focus on how to create a loop so we can stay in this cycle as long as the user wants:

  1. Prompt the user for input
  2. Read their input
  3. Handle that input (presumable by calling our database functions)
  4. Go back to Step 1

Because both promptUser and readInput both return Try values, the way we do this in Scala is to perform that loop with a for expression. So far, our main method starts like this:

@main
def ToDoList = 
    val datafile = "./ToDoList.dat"
    val db = Database(datafile)
    val processor = InputProcessor(db)

So after this, I’ll define a method here that I’ll name mainLoop:

def mainLoop(): Try[Unit] = for

From past experience I know that mainLoop will not require any input parameters, and it will also return Unit wrapped in a Try. I’ll explain this more shortly, but let’s push on for the moment.

Prompting for input

So now our mindset is that we’re writing our main loop. Therefore, the first thing we want to do is prompt the user for their input, so I add this line:

def mainLoop(): Try[Unit] = for
    _ <- promptUser()

I showed this syntax earlier, so I’ll just say that this can be read as, “Prompt the user for their input. Because promptUser returns a Unit value inside a Try, I don’t care about that value, so ignore it.” We use the _ character on the left side of the <- operator to say, “I don’t care about this value.”

Reading the input

The next thing we want to do in the loop is to read the user’s input, so I add that in:

def mainLoop(): Try[Unit] = for
    _     <- promptUser()
    input <- readInput()

This binds the user’s input to the variable I named input. Whatever they type in before they press the [Enter] key is going to be a String that’s assigned to the input variable.

Handling the input

Next, our application needs to process the user’s input. As I showed in the introduction to this To-Do List example, a user types this to add a new task with the value "Wake up":

a Wake up

and they type this to delete the first task:

d 1

and so on; they use the other commands we allow.

So now we need some sort of processor or handler to process these strings that we receive. For now I’m not going to implement this processor; I’ll just create a “do nothing” function that has the correct signature.

As usual, I start by sketching the signature of the desired function. I’ll call this function handleUserInput, and I know it’s going to take a string input parameter:

def handleUserInput(input: String)

At this point — to complete the function signature — you have to think forward a little bit: handleUserInput is going to receive the user input as a string, it’s going to attempt to process it, and then interact with our database. Because our database functions all return Try, this function should also return Try, so I add that:

def handleUserInput(input: String): Try
                                    ---

Next, because this function will always result in printing some output to STDOUT, it will have no return value, or at least no return value that we are interested in. Whenever this is the case, we declare that the function returns Unit, a Unit wrapped in a Try:

def handleUserInput(input: String): Try[Unit]
                                    ---------

And now — because I don’t want to process that user input yet — I’ll just implement this function with a println statement:

def handleUserInput(input: String): Try[Unit] = 
    // todo: implement this function
    println("handleUserInput called")

Back to the main loop

At this point we could add that function call to our mainLoop function, like this:

def mainLoop(): Try[Unit] = for
    _     <- promptUser()
    input <- readInput()
    _     <- handleUserInput(input)
    // hmm, still need to do something here ...
yield ()

Note that here I do a few things. First, handleUserInput doesn’t return anything interesting, so I use the _ on the left side of the <- symbol.

Second, I added a yield () to the end of the for expression. The () characters represent the one and only Unit value in Scala. Put another way, any time you need to return a Unit value from a function, you return (). (That is, () is an instance of Unit, its one and only instance.)

So, yield () means that this for expression returns Unit. But, because the expression works with Try values, that Unit value is wrapped in a Try. That’s why the function declaration begins like this:

def mainLoop(): Try[Unit] = for ...

The problem

However, the problem we have is that if we call mainLoop right now, it won’t really loop; it only runs once. It prompts the user, reads their input, processes it, and then quits. We still need some way for our loop to, well, loop.

When you’re working with immutable variables, the solution to this problem is almost always the same: recursion. Inside our for expression we just need to call mainLoop, so it invokes itself again. I’ll format that code so it looks like this:

def mainLoop(): Try[Unit] = for
    _     <- promptUser()
    input <- readInput()
    _     <- { 
                handleUserInput(input);
                mainLoop()
             }
yield ()

You can format it in different ways, but the important thing is that this code:

_     <- { 
            handleUserInput(input)
            mainLoop()
         }

can be read as, “Everything inside the curly braces is one block of code. Inside that block, first handle the user’s input. Then, when that function returns, call mainLoop again.”

So now mainLoop is called again, and it starts a for expression, and it’s first step is to prompt the user again for their input.

And this is a key thing to know:

When you’re working with immutable variables and need to perform a loop, you use recursion instead.

In other “looping” situations you can use Scala’s collections methods instead of recursion, but for a situation like this, recursion is the answer.

The complete main method code

Now we can look at the complete main method. The only thing new that I add to it here is an initial call to mainLoop to get things running. I note this with a comment near the bottom of this code.

@main
def ToDoList = 

    val db = Database("./ToDoList.dat")
    val processor = InputProcessor(db)

    def mainLoop(): Try[Unit] = 
        for
            _     <- promptUser()
            input <- readInput()
            _     <- {
                        handleUserInput(input)
                        mainLoop()
                      }
        yield
            ()

    // this starts the application running:
    mainLoop()

end ToDoList

Notice that I define mainLoop inside the ToDoList function, which is our application’s “main method.” It’s not necessary to define mainLoop inside ToDoList, but I do it just to show you another thing you can do with Scala.

The way this code works is:

  • Scala sees @main before the ToDoList method, and recognizes it as a main method
  • A main method is the entry point to the application, so this function starts running
  • The first couple of lines are read
  • The mainLoop function definition is read
  • The mainLoop function is run as the last line of ToDoList, and this starts the process of prompting the user and reading their input

And now that we have a loop, we can go back to work on processing the user’s input.

Update: All of my new videos are now on
LearnScala.dev