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:
- Prompt the user for input
- Read their input
- Handle that input (presumable by calling our database functions)
- 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 theToDoList
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 ofToDoList
, 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