Subing a sub

Keywords: 

You just saw how to take a program and break it into subs. Often, you go further. You break subs into subs.

Pounds to kilos again, again

Here's the structure of the pounds to kilos program again:

  • Input
    • Get and validate pounds
    • Get and validate ounces
    • Test if both inputs are zero
  • Processing
  • Output

We made subs for input, processing, and output. The input sub contained all three steps in the list.

Look at the list again, and you'll see two items that are almost identical:

  • Input
    • Get and validate pounds
    • Get and validate ounces
    • Test if both inputs are zero
  • Processing
  • Output

When you see that, you should think to yourself, "Self, that looks like a candidate for reuse. Maybe I could write one sub to do both things."

Here's the getInput sub again:

  1. 'Input and validate pounds and ounces
  2. 'Params:
  3. '  pounds: value user entered (output).
  4. '  ounces: value user entered (output).
  5. Sub getInput(pounds As Single, ounces As Single)
  6.     Dim userInput As String
  7.     'Validate pounds
  8.     userInput = Cells(3, 2)
  9.     If Not IsNumeric(userInput) Then
  10.         MsgBox "Sorry, pounds must be a number."
  11.         End
  12.     End If
  13.     If userInput < 0 Then
  14.         MsgBox "Sorry, pounds cannot be negative."
  15.         End
  16.     End If
  17.     pounds = userInput
  18.     'Validate ounces
  19.     userInput = Cells(4, 2)
  20.     If Not IsNumeric(userInput) Then
  21.         MsgBox "Sorry, ounces must be a number."
  22.         End
  23.     End If
  24.     If userInput < 0 Then
  25.         MsgBox "Sorry, ounces cannot be negative."
  26.         End
  27.     End If
  28.     ounces = userInput
  29.     'Check if both are zero.
  30.     If pounds = 0 And ounces = 0 Then
  31.         MsgBox "Sorry, please enter pounds, ounces, or both."
  32.         End
  33.     End If
  34. End Sub

Almost the same

The code for "Get and validate pounds" and "get and validate ounces" is almost identical. Here are the differences:

  1. 'Input and validate pounds and ounces
  2. 'Params:
  3. '  pounds: value user entered (output).
  4. '  ounces: value user entered (output).
  5. Sub getInput(pounds As Single, ounces As Single)
  6.     Dim userInput As String
  7.     'Validate pounds
  8.     userInput = Cells(3, 2)
  9.     If Not IsNumeric(userInput) Then
  10.         MsgBox "Sorry, pounds must be a number."
  11.         End
  12.     End If
  13.     If userInput < 0 Then
  14.         MsgBox "Sorry, pounds cannot be negative."
  15.         End
  16.     End If
  17.     pounds = userInput
  18.     'Validate ounces
  19.     userInput = Cells(4, 2)
  20.     If Not IsNumeric(userInput) Then
  21.         MsgBox "Sorry, ounces must be a number."
  22.         End
  23.     End If
  24.     If userInput < 0 Then
  25.         MsgBox "Sorry, ounces cannot be negative."
  26.         End
  27.     End If
  28.     ounces = userInput
  29.     'Check if both are zero.
  30.     If pounds = 0 And ounces = 0 Then
  31.         MsgBox "Sorry, please enter pounds, ounces, or both."
  32.         End
  33.     End If
  34. End Sub
So the differences are:

  • Which row the value comes from (3 or 4).
  • The word used in the error messages ("pounds" or "ounces").
  • Where the value goes (to pounds, or to ounces).

Let's see if we can create one sub we can reuse, first to validate pounds, and then to validate ounces.

Reduce, reuse, recycle

Let's begin by taking one of the two blocks of code, and giving it a name. We'll also replace the things that are different with some markers. I'll leave out the comments for now.

  1. Sub getPositiveNumber()
  2.     userInput = Cells(row, 2)
  3.     If Not IsNumeric(userInput) Then
  4.         MsgBox "Sorry, label must be a number."
  5.         End
  6.     End If
  7.     If userInput < 0 Then
  8.         MsgBox "Sorry, label cannot be negative."
  9.         End
  10.     End If
  11.     value = userInput
  12. End Sub
Hmmm. How to get different values for the markers? With our good friends, params! Let's make the code this:

  1. Sub getPositiveNumber(row As Integer, errorLabel As String, value As Single)
  2.     userInput = Cells(row, 2)
  3.     If Not IsNumeric(userInput) Then
  4.         MsgBox "Sorry, " & errorLabel & " must be a number."
  5.         End
  6.     End If
  7.     If userInput < 0 Then
  8.         MsgBox "Sorry, " & errorLabel & " cannot be negative."
  9.         End
  10.     End If
  11.     value = userInput
  12. End Sub
For getPositiveNumber to work for pounds, we match up:

  • row with 3.
  • errorLabel with "pounds".
  • value with pounds.

For getPositiveNumber to work for ounces, we match up:

  • row with 4.
  • errorLabel with "ounces".
  • value with ounces.

We match things up by the way the sub is called.

  1. (caller)
  2. getPositiveNumber 3, "pounds", pounds
  3. getPositiveNumber 4, "ounces", ounces
  4. ...
  5. Sub getPositiveNumber(row As Integer, errorLabel As String, value As Single)
Remember that the params in a call to a sub have to match the params in that sub's signature in number and type, but not name. The sig for getPositiveNumber has three params, so calls to getPositiveNumber must have three params. The sig for getPositiveNumber has an Integer param, then a String, and then a Single. So calls to getPositiveNumber must have an Integer, then a String, and then a Single.

The first call will put 3 into the row param, "pounds" into the errorLabel param, and match up pounds and value. Whatever getPositiveNumber puts into value, it is really putting into pounds in the caller.

The second call will put 4 into the row param, "ounces" into the errorLabel param, and match up ounces and value. Whatever getPositiveNumber puts into value, it is really putting into ounces in the caller.

Here's how getInput ends up:

  1. 'Input and validate pounds and ounces
  2. 'Params:
  3. '  pounds: value user entered (output).
  4. '  ounces: value user entered (output).
  5. Sub getInput(pounds As Single, ounces As Single)
  6.     'Validate pounds
  7.     getPositiveNumber 3, "pounds", pounds
  8.     getPositiveNumber 4, "ounces", ounces
  9.     'Check if both are zero.
  10.     If pounds = 0 And ounces = 0 Then
  11.         MsgBox "Sorry, please enter pounds, ounces, or both."
  12.         End
  13.     End If
  14. End Sub
The blocks of repeated code are gone, moved into the sub getPositiveNumber. getPositiveNumber is called twice. The new getInput is simpler, and more reliable.

Jeremy
Jeremy
I don't get it. Why is getInput more reliable?

Tara
Tara
There's less code, so less to go wrong. Less testing. Less screaming.
What makes this work is the fact that variables in a call to a sub, like…

getPositiveNumber 3, "pounds", pounds

don't have to have the same names as params in the sub's signature…

Sub getPositiveNumber(row As Integer, errorLabel As String, value As Single)

If they had to be the same, it would be harder to reuse code.

The param's data types have to match the data the caller gives. getPositiveNumber() takes an integer, then a string, then a single. When we call it, we have to give it an integer, a string, and a single. In that order.

Also notice that as long as the caller gives the sub the right type of value, it doesn't have to be a variable. It can be a constant, like 3, or "ounces".

So:

  • Call and sub must have the same number of params.
  • Call and sub must use the same data types, in the same order.
  • Call and sub don't have to use variables at all, or variables with the same name.

It's the data types that must match.

Summary

When you see two blocks of code that are almost identical, you should think to yourself, "Self, that looks like a candidate for reuse. Maybe I could write one sub to do both things."

Taking one of the two blocks of code, and give it a name. Replace the values that are different between the blocks of code with variables. Pass their values as parameters.