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
Blackhat function definition
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.
Functions with no parameters and default parameters
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
Syntax for calling functions and passing arguments
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
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.
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.
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
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
- 1.
Given a number as an input, return a Boolean that denotes whether or not that number is an even number.
- 2.
Given a first name and a last name, return a string that is considered the full name of an individual.
- 3.
Given a width and a height of a rectangle, return the area of that rectangle.
- 4.
Given a list as input, return the first element of that list.
- 5.
Given a list as input, return the last element of that list.
- 6.
Given a list of words, return the largest word.
- 7.
Given a list of words, return the length of the smallest word.
- 8.
Given a list of integers, return the sum of all integers.
- 1.
Create a function for each pattern matching condition.
- 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.