home search about rss feed twitter ko-fi

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 a Failure, and the for expression stops here, and maybeNumRowsDeleted will have that Failure value.
  • The Try is a Success, and the variable rows is a Seq[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