App 2: delete Database Function (Scala 3 Video)
When you think about it, a delete
function should either take (a) a string that represents the To-Do Item the user wants to delete, or (b) an index (row number) that should be deleted. Either approach is okay, but after I worked with the UI a little bit, I decided to go with the index-based approach, so I’ll share that here.
delete
So far we know that we want a delete
function that takes an index, so we have this:
def delete(indexToDelete: Int)
We also know that because it accesses the outside world, it should return a Try
, so I add that:
def delete(indexToDelete: Int): Try
---
And finally, because SQL delete
queries return a count of the number of rows that are deleted, I’ll do the same thing, returning that count inside the Try
:
def delete(indexToDelete: Int): Try[Int] =
--------
Technically I’m only allowing the user to delete one task a time, so this information isn’t terribly useful. But, if you want to change the app to let the user delete multiple tasks at once, that value will be more useful.
Writing the function body
Having written functions like this before, I know that the function body should work something like this:
- Read the entire flat-file database into a list of strings
- Remove the matching string from that list (using the index value)
- Write the remaining list back to the file (overwriting the file, rather than appending to it)
There are a few ways to do this, but one approach is to use a for
expression. Because I haven’t shown those much yet, I’ll use that approach here. In this case I’m going to share the complete solution, and then describe how it works:
def delete(indexToDelete: Int): Try[Int] =
val maybeNumRowsDeleted = for
rows <- selectAll()
newRows = CollectionUtils.removeElementByIndex(rows, indexToDelete)
numRowsDeleted = rows.size - newRows.size
_ <- writeToFile(newRows, false)
yield
numRowsDeleted
maybeNumRowsDeleted
If you haven’t seen this type of code before — fear not! — I’ll explain how it works.
Getting rid of the function signature for the time being, the code starts with the result of a for
expression being assigned to a variable:
val maybeNumRowsDeleted = for
I know that this for
expression returns the Try[Int]
type, so I can also explicitly declare that here:
val maybeNumRowsDeleted: Try[Int] = for
--------
However, I’ll leave that off the following examples.
The body as a for-expression
As mentioned in lesson on for
expressions, a for
expression always starts with a generator, and in our case the selectAll
function serves as that generator:
val maybeNumRowsDeleted = for
// start with a generator:
rows <- selectAll()
Recall that selectAll
returns a Try[Seq[String]]
, so at this point one of two things happens:
- That
Try
is aFailure
, and thefor
expression stops here, andmaybeNumRowsDeleted
will have thatFailure
value. - The
Try
is aSuccess
, and the variablerows
is aSeq[String]
.
There’s nothing for us to do about the Failure
case, it’s already handled for us, so we continue to write our code assuming we received a Success
, and we continue on down the happy path.
What I want to do next is delete the element in that Seq[String]
according to its index value. (Recall that this function gets that index passed to us.) So now what I do inside the for
expression is remove that element from rows
. This process yields a new Seq[String]
, which I name newRows
:
val maybeNumRowsDeleted = for
rows <- selectAll()
// remove the desired row, using its index value:
newRows = CollectionUtils.removeElementByIndex(rows, indexToDelete)
If my requirements were different I could just write newRows
back to the database file, and we’d be done. But my requirements state that I need to return the number of rows that were deleted. A simple way to do that is to subtract the size of newRows
from rows
. In theory — because this function only takes one index value, newRows
should always be one row smaller — but if something is wrong with that index, it is possible that the result could be 0
instead of 1
.
Going back to the for
expression, I add an expression to count the number of rows that were actually deleted:
val maybeNumRowsDeleted = for
rows <- selectAll()
newRows = CollectionUtils.removeElementByIndex(rows, indexToDelete)
// count the number of rows deleted:
numRowsDeleted = rows.size - newRows.size
Finally, I can write newRows
to the database file. Remember that writeToFile
returns a Try
, so in real-world code you’ll want to pay attention to that result. But for the purposes of this for
expression, I thought it would be more interesting to show what to do if you don’t care about the return value of an expression inside a for
expression, so I wrote this:
_ <- writeToFile(newRows, false)
The underscore character on the left side of this expression is the Scala way of saying that inside a for
expression, “I don’t care about this resulting value, so you can just discard it.” Again, in the real world you’ll be more interested in that Try
, but I wanted to show this technique.
When I add this in, the for
expression now looks like this:
val maybeNumRowsDeleted = for
rows <- selectAll()
newRows = CollectionUtils.removeElementByIndex(rows, indexToDelete)
numRowsDeleted = rows.size - newRows.size
// ignore the resulting value:
_ <- writeToFile(newRows, false)
Finally, because our delete
function needs to return the number of rows that were deleted, we yield that value from the for
expression, and it is bound to the variable named maybeNumRowsDeleted
:
val maybeNumRowsDeleted = for
rows <- selectAll()
newRows = CollectionUtils.removeElementByIndex(rows, indexToDelete)
numRowsDeleted = rows.size - newRows.size
_ <- writeToFile(newRows, false)
// yield the count of rows deleted:
yield
numRowsDeleted
Note that you can also explicitly declare maybeNumRowsDeleted
’s data type like this, if you prefer:
val maybeNumRowsDeleted: Try[Int] = for ...
--------
I’ve found that modern IDEs show that type for you, so in your code it isn’t completely necessary. But when you’re reading this code in a book, it can be more helpful to see that type here.
After this — in the final line of the delete
function — I return this count:
maybeNumRowsDeleted
Technically the maybeNumRowsDeleted
variable isn’t needed. If you prefer to not use it, you can just return the result of the for
expression as the result of the function, like this:
def delete(indexToDelete: Int): Try[Int] =
for
rows <- selectAll()
newRows = CollectionUtils.removeElementByIndex(
rows, indexToDelete
)
numRowsDeleted = rows.size - newRows.size
_ <- writeToFile(newRows, false)
yield
numRowsDeleted
I suspect that most experienced Scala developers use this approach, but I showed the other approach first because I think it might be a little easier for people who are new to Scala.
So this is our complete delete
function.
Note
If you’d like to see how the removeElementByIndex
function is implemented in the CollectionUtils
object, see the “Generics” lesson in the Appendix.
Update: All of my new videos are now on
LearnScala.dev