© Jason Lee Hodges 2019
J. L. HodgesSoftware Engineering from Scratchhttps://doi.org/10.1007/978-1-4842-5206-2_7

7. Functions

Jason Lee Hodges1 
(1)
Draper, UT, USA
 

Famous science fiction writer, Arthur C. Clarke, once wrote that any sufficiently advanced technology is indistinguishable from magic. If such a technology is indeed considered magic, its software engineers would most assuredly be viewed as its magicians. You might be familiar with some of the prototypical tricks that a magician might perform. Often their repertoire would include spectacular acts of spontaneous materialization or object transformation, through means of a magical black top hat as a medium, sprinkled with the citation of a few magic words. To extend the magical analogy, in software engineering, the magical black top hat would be considered a function and the function’s name might be deemed the magic word.

Functions in programming are simply special expressions that can take input and return output. In this way they are much like the magic black top hat. Perhaps a handkerchief might go into the hat and “Presto Change-o!” a dove will come back out. What happened inside the hat is obfuscated from the audience. All they need to know is that, given a handkerchief as an input, a dove will be produced as an output. The suppression of the material details of what happened in the black hat is called abstraction. You might often hear the concept of abstraction referred to as a “black box” since you cannot see what’s happening inside. Functions abstract the implementation details from your code and wrap them up into a function name so that you do not have to worry about re-implementing previously written code each time you need to use the function. The reusability of a function is one way to create a concept known as modularity in your code, by segregating common pieces of functionality into function code. Modularity and abstraction are the two main purposes of functions.

There are several built-in functions in the Scala language that you have seen already such as println and readLine. However, you can also define your own functions. In this chapter, you will be introduced to the creation of custom functions, how to use a custom function, and some of the benefits of using functions in your code. You will also see how to apply this new functionality to our example operating system.

Function Definition

Functions can be created in Scala using the def keyword that stands for “definition.” Creating a function is said to be defining a function. You might also hear it referred to as declaring a function. The def keyword is followed by the name of the function or the magic word that you wish to use to produce a given output. It is a best practice to use camel case when naming a function (capitalizing each word in a phrase except the first word and excluding any spaces, i.e., blackHat). After the function name is provided, Scala will look for parentheses that take in parameters much like the println and readLine functions. Within the parentheses, you provide a comma-separated list of input parameter names and their data types. The input parameter names can then be used later in the scope of the function much like a variable placeholder. After the parentheses you can optionally provide a colon and then the data type of the value that will be returned. If you do not provide the return type, Scala will infer it for you, but as mentioned previously, it is a good practice to always define your types for better control and optimization. After the return type, you provide an equal sign and a pair of curly braces that hold the scope of the function. Figure 7-1 illustrates our magic hat example as a reference.
../images/476847_1_En_7_Chapter/476847_1_En_7_Fig1_HTML.png
Figure 7-1

Illustration of the input and output capabilities of a function

Within the scope or body of the function definition, you provide the implementation details of what you want the function to do with the given input. You can manipulate the input parameters in any way you like. When you have completed the function’s implementation details, you use the return keyword to return a value back as output from the function. If you do not use the return keyword, Scala will infer that the evaluated expression in the last line of your function body should be the returned value. Figure 7-2 shows an example of how we might define a basic function and also provides a breakdown of each part of the syntax for our magic black hat example.
../images/476847_1_En_7_Chapter/476847_1_En_7_Fig2_HTML.jpg
Figure 7-2

Definition and key pieces of function syntax

Listing 7-1 provides the unmarked code version of this same example written in an examples.sc Scala script that you can execute to ensure that the compiler correctly recognizes the syntax. You’ll notice that, just as control flow constructs maintain scope independent variable assignment, variables assigned within a function’s scope cannot be accessed outside of the function. In other words, the variables defined within the function body are said to have a local scope. You can also think of the input parameters as variables with a scope that is local to the function.
examples.sc
def blackHat(inputObject: String): Any = {
   if(inputObject.equalsIgnoreCase("handkerchief")) {
       return "Dove"
   }
}
Listing 7-1

Blackhat function definition

It should be noted that as soon as a branch of code in your function reaches a return statement, the rest of the code within the function will cease to execute. As soon as the function returns an output, the function’s process is complete. To illustrate this concept, Listing 7-2 demonstrates an example where two return statements are provided.
examples.sc
def magician(name: String): String = {
    if(name.equalsIgnoreCase("David")){
        return "David Copperfield"
    }
    return "Harry Houdini"
}
Listing 7-2

Function with multiple return statements

In this example, if the input string parameter name is “David,” the function will return the string “David Copperfield” and ignore the second return statement. If the string does not equal “David,” the conditional branch will be ignored and the final line will be executed returning “Harry Houdini.” In this function, the return keyword could be removed before the string “Harry Houdini” as Scala would imply that the last line is the expression that should be returned as long as it matches the return type that is defined in the first line of the function definition. However, as a best practice while you are starting out, it is better to explicitly provide return statements.

Worthy of note is the fact that functions are not required to have parameters at all, and if they do have parameters, they can be made to be optional. If there are no parameters, Scala still requires parentheses; they will simply contain no characters in between. If you wish to make the parameters optional, you must provide a default value by succeeding the parameter type with an equal sign and a literal value that represents what you want the default value to be. By providing a default value, Scala will implicitly understand that if the value is not provided, it should use the default value provided in the function definition and you can handle any subsequent default logic surrounding that parameter in your corresponding function body. Listing 7-3 provides examples of functions with no parameters and functions with both optional and required parameters.
def generateDove() = {
    "Dove"
}
def generateBirds(numOfBirds: Int, typeOfBird: String = "Dove") = {
    s"${typeOfBird} " * numOfBirds
}
Listing 7-3

Functions with no parameters and default parameters

Notice in this example that the return keyword and the return type have been removed for the sake of demonstrating implicit return options. The first function, which has no parameters, will always return the string “Dove” as an output. The second function takes an integer as its first parameter that is required (since no default value is provided to it) to tell the function how many birds to generate. The second parameter is an optional parameter. If no parameter is provided, the function will return a string that contains the number of “Doves” required by the numOfBirds parameter delimited by a space character. If a different string is provided to the typeOfBird parameter, the function will return that string multiplied by the numOfBirds instead of the default “Dove” string. Both of these functions implicitly return a String even though no return type was specified. But, functions can also effectively return nothing as demonstrated by Listing 7-4.
def presto(): Unit = {
    println("Poof!")
}
Listing 7-4

Function that does not return a value

Observe that although this function is an example of a function that does not return a value, a return type is specified. That return type is Unit which is essentially a stand in data type for “Nothing.” It tells Scala that the function is not expected to return a value at all. You might hear other languages might refer to this type as void. So, if this function does not return a value, then what is its purpose? Functions with no return type typically exist to perform some type of “side effect.” You will learn more about side effects in the chapter that describes the functional programming paradigm, but for now just know that functions with no return value perform an action in your code that does not require a response from your expression. In this example that action is a println function that prints out a string value to the console. Other examples of functions that might not require a return type might include functions that save a file to your computer or write data to a database. But even in those scenarios it is often useful to receive a return value back from the function indicating whether or not the function was successful. Thus, it is a best practice to always have a function return some type of value if possible.

Calling a Function

Now that you know how to define your own custom function, you can now use it as many times as you like in your code. Using a function in your code is known as “calling” a function. You might also hear it referred to as invoking a function or executing a function. A function call is said to “take” arguments. An argument is the value that you provide to the function as an input to satisfy its parameter definition. Arguments and parameters are often confused for one another. Just remember that the locally scoped variable that is created when defining the function is the parameter and the value given to the function at the time that it is called in your code is the argument. Providing arguments to a function call is known as “passing” the arguments to the function. Arguments can be literal values, expressions, or variables. Examples of these different argument usages and the syntax for calling various functions are provided in Listing 7-5 using the functions defined in Listing 7-3.
examples.sc
var birds1 = generateBirds(1)
var birds2 = generateBirds(1 + 1, "Finch")
var three = 3
var birds3 = generateBirds(three, generateDove())
var birdList = List(birds1, birds2, birds3)
for(bird <- birdList) println(bird)
Terminal Output:
Dove
Finch Finch
Dove Dove Dove
Listing 7-5

Syntax for calling functions and passing arguments

As you can see, the first function call only passes an argument to the function for the required parameter. Because only one argument is provided and the second parameter is optional, there is no need to add a comma and pass another argument to the function; Scala will simply assume that the literal value for that second parameter should be the default value of “Dove.” In the second function invocation, instead of passing a literal value for the first argument, an expression is passed. That expression will evaluate to 2 in order to satisfy the integer type requirement from the function definition. You can also see that a second argument is provided in the parentheses to explicitly set the type of bird that is going to be generated as “Finch.” The third function execution uses a variable to pass to the required parameter. That variable, three, evaluates to the integer 3 to satisfy the requirement from the function definition. After that a function is passed for the second parameter. That function call is a function that does not take any arguments, but notice that it still requires that you provide the parentheses in order to invoke the function execution. That function returns a value of “Dove” that is then passed as the second parameter to the function. You would be wise to observe that the second parameter would have evaluated to “Dove” even if you did not pass the generateDove() function to it as the second argument. However, it was used in this case to explicitly demonstrate that functions can be passed as arguments assuming that their return value will satisfy the parameter type required by the function definition that they are being passed to. The last few lines of code simply wrap the variables up in a list so that it is easily to loop through them with a for loop and print their evaluated values out to the console for the user to see. You just as easily could have called the functions directly in the instantiation of the list and avoided the three variable assignments. Listing 7-6 demonstrates that refactor.
var birdList = List(generateBirds(1), generateBirds(1 + 1, "Finch"), generateBirds(three, generateDove()))
for(bird <- birdList) println(bird)
Listing 7-6

Functions called directly in the instantiation of a list

This example will yield the same exact terminal output as Listing 7-5. As you might deduce, this demonstrates that custom functions evaluate to their return value just like expressions do and can be used throughout your code anywhere where an expression could be used. This yields several extremely valuable benefits that continually recur throughout the common paradigms in computer science.

Benefits

Functions in programming provide two demonstrable key benefits that will be illustrated in the coming code listings. The first is the concept of abstraction that obfuscates implementation details and reduces code duplication. The second is modularity or decomposition which breaks your code into logically separated pieces for greater organization and readability. Both abstraction and modularity provide the added benefit of being easily tested which provides for the long-term maintainability of your code base.

Abstraction

In the 2006 film The Prestige, two 19th-century rival magicians write entries in their diaries that include details about how to perform their tricks. In order to ensure that no one can read their diaries and discover their magical methods, they ensure that every entry is encoded as a cipher. An example of a ciphered message is presented in Listing 7-7 along with the implementation details surrounding how to decipher the message.
var cipher = new StringBuilder("Ue:h5jf6& Wit Set d6'r!cfjet6,!vli3dn9lp!cs5uetdt9ds5jf 7swa!een66pcb Eepcen5ffdd.ds!ctT6gi,9eda!dwte5s3!i:kd4kkdd!aey55pcbue9!vbu%vtatew4!aei6.uce Fag5koeas69 Waj5isbd!cbe7nw.te!asd4ywau66hdajd4nwahce.b5")
var shiftedEncoding = List.newBuilder[Int]
for(i <- Range(0, cipher.length)){
    if(i % 2 == 0){
        shiftedEncoding += i
    }
}
var positionalEncoding = List.newBuilder[Int]
for(i <- Range(0, cipher.length)){
    if(i % 3 == 0){
        positionalEncoding += i
    }
}
for(i <- Range(0,cipher.length)){
    if(shiftedEncoding.result().contains(i)){
        cipher(i) = (cipher(i)-1).asInstanceOf[Char]
    }
}
var message = ""
for(i <- Range(0,cipher.length)){
    if(positionalEncoding.result().contains(i)){
        message += cipher(i)
    }
}
println(message)
Listing 7-7

A ciphered message and its corresponding decoding algorithm

There are a couple of new concepts here that you haven’t seen yet, but despite the fact that this implementation could have been solved in a much more elegant way, I wanted to stick to the concepts you already know for this example as much as possible. The two main things you haven’t seen are the StringBuilder and the newBuilder function that is a member of the List data structure. These will likely make more sense to you later on in the book. However, for now all you need to know is that they are necessary to ensure that the cipher string and the two lists of encoding integers can be mutated.

The first line of code defines a new variable called cipher that uses the string builder to create a garbled sequence of characters in a string. The characters in this string will need to be modified in order to decipher the message and, in order to modify the individual characters, a string builder was necessary. Next, two variables are defined, shiftedEncoding and positionalEncoding. Both variables are assigned to a list builder that creates a mutable list that can easily be added to using the += operator. Next, both of the lists run through a for loop that spans the same length as the cipher and adds any numbers that match a modulo operation to the list. For the shiftedEncoding list, the modulo is checking to see if the position is divisible by 2, and for the positionEncoding list, the modulo is checking to see if the position is divisible by 3. After these two for loops end, each list contains the positions from the cipher that match the modulo condition. So shiftedEncoding will contain 0, 2, 4, 6, and so on, and positionEncoding will include 0, 3, 6, 9, and so on. You’ll notice that the steps that were taken for both lists were almost exactly the same besides which integer to use when creating the conditional statement with the modulo operator. Any time you see code that is repetitive it should trigger an alert in your head that there is an opportunity for abstraction. Repeated code is what is known as a “code smell,” which is the notion that a bad practice is being used in code that is in need of obvious refactoring. A common mnemonic device in programming is DRY, which stands for “Don’t Repeat Yourself.” That being said, it’s pretty obvious that we can refactor this cipher code using functional abstraction; however, let’s finish walking through the deciphering algorithm first.

After the two encoding lists are created, the code runs through the length of the cipher in a for loop again and checks to see if each position in the cipher exists in the shiftedEncoding list. If it does exist, the character in the string at that position is shifted back one spot in the alphabet (hence the need for the StringBuilder). So, if there is a 'b' at that position, it will be mutated into an 'a'; if it’s a 'z', it will be mutated into a 'y'; and so on. Because of the nature of the modulo 2 in the creation of the shiftedEncoding list, every other character in the cipher will be shifted back one letter in the alphabet.

Finally, a message variable is defined and set to an empty string. Then we loop through the length of cipher yet again and pull out any character that has a position that exists in the positionalEncoding list and add it to the message string. Essentially, after every other character has been shifted, every third character is then selected to be in the final message. After the loop has been completed and the message variable has added every third character, the message variable is printed to the console to unveil the complete deciphered message. Try running this code on your own to discover what the secret message is. Listing 7-8 illustrates how we might pull some of the common code out of this linear procedural program and create a function to demonstrate abstraction (the cipher string has been replaced with an ellipsis for brevity).
...
def findPositions(mod: Int): List[Int] = {
    var listBuilder = List.newBuilder[Int]
    for(i <- Range(0, cipher.length)){
        if(i % mod == 0){
            listBuilder += i
        }
    }
    return listBuilder.result()
}
var shiftedEncoding = findPositions(2)
var positionalEncoding = findPositions(3)
for(i <- Range(0,cipher.length)){
    if(shiftedEncoding.contains(i)){
        cipher(i) = (cipher(i)-1).asInstanceOf[Char]
    }
}
var message = ""
for(i <- Range(0,cipher.length)){
    if(positionalEncoding.contains(i)){
        message += cipher(i)
    }
}
println(message)
Listing 7-8

Deciphering algorithm refactored to abstract common functionality into a function

Notice that the common functionality has been removed from beneath the creation of the two encoding variables and written one time in the body of a function called findPositions. The findPositions function takes one argument which is the integer that you want to use in the conditional statement with the modulo. It creates a list and fills it with integers that match the modulo operation. Now that the function is declared, you could reuse this function over and over again throughout your code and only have to write the definition once. By calling it, you could generate a list that is every 4th integer, every 5th integer, and so on. Also, once you know what input you have to provide to the function and what it will return, you can completely forget about what it does to create that return value. All you need to care about is that you can pass it an integer and it will return a list of integers. Other developers will also be able to use your function in their code without ever needing to fully understand the implementation details in the body of the function. Based on that, you should be able to recognize the benefit of this type of reusability that is inherent with an abstracted piece of code.

Once the repeated code has been abstracted, you can see that the function is called twice when assigning values to the two encoding variables, each with a different argument being passed to the function. You might also notice that the result() function that was being called on the encoding lists when checking if the position is contained in them has been removed from the conditional statement and added into the function in the return statement.

Exercise 7-1

See if you can identify any additional repeated code in Listing 7-7 that can be abstracted into functions without changing the final result of this program.

Modularity

In the introduction to that same 2006 film, it is stated that every magic trick has three parts. First is the pledge, where the magician shows you something ordinary. Next is the turn, where the magician makes that something ordinary do something extraordinary. And finally the prestige, where the magician makes the extraordinary thing return to its ordinary state. These three parts or modules are individual pieces that compose an overall composition: the magic trick. Breaking down the trick into its component pieces is called decomposition, which is a key advantage to using functions and allows a programmer to organize their code for long-term maintainability. If your program is the overall magic trick, then breaking it down into three functions called pledge(), turn(), and prestige() is the perfect example of decomposing code for modularity.

Modularity is the notion that a component of code can be isolated and extracted from its program and reused in any other program as an individual module. By isolating code to an individual module, the code can be easily tested. You can give it several inputs with the expectation that you know what the outputs should be. Then, independent of the rest of your program, you can test whether the expected outputs were actually received. This is critical in creating and maintaining large systems over long periods of time. We will cover this in more detail in the chapter on testing. In our magic trick components example, if future magic trick programs needed to reuse the same pledge, turn, or prestige component, that function could be extracted and reused very easily with no extra work necessary. Thus, it could be said that creating modular components is a key strategy in writing DRY code.

So, if our cipher code has been refactored to use a function that demonstrates abstraction, could it also be said that the same function demonstrates modularity? To answer that, we must examine the code to determine whether or not the code is isolated from the rest of the program. Could we extract this code out of our cipher program and put it in another program and achieve working, testable code? The answer is no, because the function itself uses a variable, cipher, that is defined in the global scope of the program. A completely isolated and modular function uses only the inputs given to it or defined itself in order to calculate its output. Listing 7-9 demonstrates how we might refactor our cipher code to make our findPosition() function modular.
var cipher = new StringBuilder("Ue:h5jf6& Wit Set d6'r!cfjet6,!vli3dn9lp!cs5uetdt9ds5jf 7swa!een66pcb Eepcen5ffdd.ds!ctT6gi,9eda!dwte5s3!i:kd4kkdd!aey55pcbue9!vbu%vtatew4!aei6.uce Fag5koeas69 Waj5isbd!cbe7nw.te!asd4ywau66hdajd4nwahce.b5")
def findPositions(mod: Int, encryptedMessage: StringBuilder): List[Int] = {
    var listBuilder = List.newBuilder[Int]
    for(i <- Range(0, encryptedMessage.length)){
        if(i % mod == 0){
            listBuilder += i
        }
    }
    return listBuilder.result()
}
var shiftedEncoding = findPositions(2, cipher)
var positionalEncoding = findPositions(3, cipher)
...
Listing 7-9

Cipher code refactored to demonstrate modularity

Notice that the definition of the findPosition() function now includes a parameter called encryptedMessage that is used to take in whatever message you are looking to find positions on. Then the for loop uses that parameter instead of the global variable cipher to determine the length of the loop. Finally, you’ll notice that when the functions are called, the cipher variable is passed to the function as the second argument to obtain the same functionality that previously existed. But now the function operates independently and modularly. If you took the findPositions() function out of this program and put it in another program, it could be used to find positions for any message that was built with a StringBuilder and required a list of positions as an output.

Application

Now that you know how to define and call a function, let’s refactor our Nebula Operating System to allow for the use of this great new tool. In our last example of this command-line shell, we started to actually build out functionality surrounding potential commands that match a certain pattern based on user input. In order to better organize the code, let’s abstract the code block that executes the addition command and place it in a function. An example of that refactor can be observed in Listing 7-10.
def addCommand(userInput: String) {
    var tokens = userInput.split(" ")
    var plusIndex = tokens.indexOf("+")
    try {
        println(tokens(plusIndex-1).toDouble + tokens(plusIndex+1).toDouble)
    }
    catch {
        case _: Throwable => println("An error occurred trying to process an addition command.")
    }
}
println("Welcome to the Nebula Operating System (NOS)! Version 1.0.3")
var command = ""
do {
    command = readLine("NOS> ")
    command match {
        case c if c.contains("+") => addCommand(c)
        case c if c.contains("-") => println(s"Subtraction command: ${command}")
        case c if c.equalsIgnoreCase("help") => println("Help Command")
        case c if c.equalsIgnoreCase("shutdown") => println("Shutting down...")
        case _ => println(s"${command} is not a known command.")
    }
}
while (!command.equalsIgnoreCase("shutdown"))
Listing 7-10

The Nebula OS shell refactored to abstract the addition command into a function

As you can see, by moving the code logic into its own function, it cleans up the pattern matching expression. All the match expression needs to know is that if the user input contains a “+” then it can pass that command along to the addCommand() function and it will handle the rest. By doing this, we can further clean up the other match conditions to make that code block even more concise and delegate the execution of each command to function code blocks. There might also be commonalities between these functions that can be further abstracted.

Exercise 7-2

Create a function for each of the following requirements:
  1. 1.

    Given a number as an input, return a Boolean that denotes whether or not that number is an even number.

     
  2. 2.

    Given a first name and a last name, return a string that is considered the full name of an individual.

     
  3. 3.

    Given a width and a height of a rectangle, return the area of that rectangle.

     
  4. 4.

    Given a list as input, return the first element of that list.

     
  5. 5.

    Given a list as input, return the last element of that list.

     
  6. 6.

    Given a list of words, return the largest word.

     
  7. 7.

    Given a list of words, return the length of the smallest word.

     
  8. 8.

    Given a list of integers, return the sum of all integers.

     
Complete the following exercises related to the Nebula OS shell:
  1. 1.

    Create a function for each pattern matching condition.

     
  2. 2.

    Identify the common functionality between the addition and subtraction functions and abstract it further so there is no repeated code.

     

Summary

In this chapter, you learned how to define a function with both optional and required parameters and a return value. You also learned how to call that function by passing arguments to satisfy the function’s defined parameters. The benefits of using functions include reusability, ease of testing for long-term maintenance, “black-box” abstraction, and extractable modularity. In the next chapter, you will learn another strategy used quite extensively to satisfy the necessity for modular code in software engineering known as classes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.131.255