Methods

A method is a named block of code with a defined purpose. Methods make source code more readable and concise, as they break it up into separate, reusable chunks. Almost all Shadow code is contained inside of methods.

Whether you noticed it or not, we have already been writing our code inside of a method: the main() method (for more information, see a previous tutorial). However, as our programs get more and more complex, putting all our code inside the main() method can make it confusing, repetitive, and hard to read. Thus, we create other methods.

Before we break down all the components of a method, let’s take a look at the following program that will guide our analysis:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import shadow:io@Console;

class Restaurant
{
    /* Imagine you are at a restaurant with 3 other friends.
     * The service was amazing, so you want to leave a 30% tip.
     * Then, you would like to split the total dining cost
     * plus tip evenly between the four of you.
     * The following program introduces methods to accomplish
     * these tasks.
     */
    public main( String[] args ) => () //the main method
    {
        var mealCost = 90.56;
        var tipPercent = 30;
        var people = 4;

        // Two example method calls are below
        var includeTip = calculateTip(mealCost, tipPercent);

        Console.print("The price per person is: $" # perPerson(mealCost + includeTip, people));
    }

    // The two methods we will discuss are below

    public calculateTip(double price, double percent) => (double)
    {
        var tip = price * (percent/100.0);
        return tip;
    }

    public perPerson(double price, int people) => (double)
    {
        return price/people;
    }
}

The console output is as follows:

The price per person is: $29.432

Method headers

First, we will break down method headers and signatures. Lines 12, 26, and 32 are the three method headers in the program. Let’s focus on the ones on Line 26 and Line 32.

  • public calculateTip(double price, double percent) => (double)

  • public perPerson(double price, int people) => (double)

As we go over each element of these headers, note that the order of these elements will not change, no matter what method you’re writing.

Access modifiers

The first thing we should notice is the reserved word public. Until we get to Classes, we will start all methods with the access modifier public (instead of private or protected). If a method is declared public, there are no restrictions on which other code can use it.

Method name

The next element we see is the method name. The same rules apply for method names as variable names. The convention is to use camel case notation, with the first word always lower case. Your method name should also be descriptive of its purpose: Someone else reading your code should have a general idea what the method does just from its name. For example, you can assume calculateTip() calculates the amount of tip.

Parameter list

The list of variables inside of the parentheses are called parameters. A method may take in no parameters, one parameter, or many parameters. It’s up to the programmer to decide. A parameter is a value passed to a method when the method is called. The method header specifies the type and order of each value that must be passed in for the method to execute. For example, the parameters for perPerson() are double price and int people. This means that when the method is called, the first value must be a double and the second value must be an int. Inside of the method, price and people become local variables initialized to the values passed in.

Return types

The last element in the method header are the return types, which specify the kinds of values that are sent back to the code that called a given method. Like parameters, Shadow allows zero, one, or more return types. Unlike parameters, these return values do not need to be given a name, but you can supply a name if you like. Names for return types have no meaning except as a form of documentation to give someone calling the method a better idea what the returned value might be.

In our example, both calculateTip() and inPerson() have a double return type. This means that if you tried to return a String in either of these methods instead of a double, you would get a compiler error. As seen in Lines 29 and 34, a return statement starts with the reserved word return and is followed by either a variable name, literal value, or some expression that results in the appropriate type. Don’t forget the semicolon at the end.

Note

A method does not need to have any return types. It could simply peform some action or call another method. When there are no return types, simply leave the parentheses empty.

Method body

Now that you understand the basic elements of a method header, let’s briefly discuss the method body – the code enclosed in braces following the method header. Within the method body you may do a number of things, including but limited to: calling another method, performing calculations, making decisions, running loops, or printing output to the console. In other words, this is where the action that the method performs takes place. If the method has specified return types, it must have a return statement at the end of the method body or the program will not compile. Conversely, if there is no return type, the method should not try to return a value, but the method can choose to use a return statement with no argument.

Any code you’ve put into a main() so far could be put into any other method. There is no limit on how long or complex a method can be, although it’s a good software engineering practice for each method to perform one specific task. If you find your method growing too long and complex, break it down into additional methods. Ideally, if a method does its job well, it can be called by other code that’s trying to accomplish the same task.

Dividing code into methods has many benefits. Each chunk of code has a straightforward task and is thus easier to read and think about. Code that is done frequently can be put into a method and called instead of copying and pasting that code throughout your program. Doing so shortens code overall, but it also means that finding a mistake in a method requires only a single fix instead of updating many places.

Calling a method

Writing a method creates code that can perform a task, but the task isn’t actually performed until the method is called. Calling a method means pausing what we’re currently doing, jumping into the code of a method (supplying the parameters it requires) and allowing it to execute. When we call a method, it might perform arbitrarily complicated taskes (including calling other methods) before returning back to the point where it was called, often with results. This idea of pausing the current execution and waiting for the method call to return seems simple but allows for very complex behavior, including recursion.

Returning to our example program, we start in the main() method. As seen in Lines 14-16, the first lines of the main() method, we have a few variables assigned initial values. In Line 19 we call calculateTip() to initialize the includeTip variable.

In order to call a method, the syntax is: methodName(parameter, parameter, ...). If the method is called on a different object, the syntax is: object.methodName(parameter, parameter, ...). Lines 19 and 21 are both examples of method calls. You may be wondering why we stored the result of one method call in a variable but used the other directly in a Console.printLine() statement. Both are syntactically correct. We stored the double value returned from the calculateTip() method in includeTip so that we could use this variable as a parameter for the other method. Once we call perPerson() we are done doing calculations, so there wasn’t a need to store the result in a variable before printing it out.

Once the program reaches a return statement, control is passed back to the calling method, in this case, the main() method. If a method returns a value and the calling code neither stores it in a variable nor uses it directly, the value is lost.

Method overloading

Within the method header, the method’s name and parameter list is considered the method signature. In the previous example, the method signature of calculateTip() was calculateTip(double price, double percent).

Why is the method signature important? Two methods cannot have identical signatures, but two methods can have the same names as long as they have a different number or type of parameters, which is called method overloading. Method overloading is usually valuable when each method is a variation on a theme, performing slightly different tasks based on the input.

The following short program is an example of method overloading:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import shadow:io@Console;

class OverloadedMethod
{
    public main( String[] args ) => ()
    {
        playLottery(8);
        Console.printLine();
        playLottery(10, "Daily Double");
    }

    public playLottery(int number) => ()
    {
        Console.printLine("Jackpot! You just won " # number # " dollars!");
    }

    public playLottery(int number, String name) => ()
    {
        Console.printLine("You're playing the " # name # "!");
        Console.printLine("Jackpot! You just won " # number # " dollars!");
    }
}

The console produces the following message:

Jackpot! You just won 8 dollars!

You're playing the Daily Double!
Jackpot! You just won 10 dollars!

Notice how in Lines 7 and 9, we make a method to call playLottery(). But if there are two methods named playLottery(), how do we know which one will run? On Line 7, the program knows to call the first playLottery() method (starting on Line 12), as its parameter list with one int matches the method call’s parameter list in type and number.

On Line 9, the method call has two parameters, an int and a String. The program then knows to call the second method, because the parameters of the call match the signature of that method. Thus, playLottery() is an overloaded method. Although method overloading can be useful, it can also become confusing, so limit overloading methods to those situations when it’s really helpful.

Returning multiple values

An unusual feature of Shadow is the ability to return multiple values from a method. The same principles of defining and calling methods apply, but the syntax changes slightly, as illustrated through the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public main( String[] args ) => ()
{
    int result, modulus, answer;
    (result, modulus) = divide(7, 3);
    (answer, ) = divide(7, 3);

    Console.printLine("Result is " # result # " and modulus is " # modulus);
    Console.printLine("Answer is " # answer);
}

public divide(int a, int b) => (int, int)
{
    int quotient = a / b;
    int remainder = a % b;
    return (quotient, remainder);
}

The console output is:

Result is 2 and modulus is 1
Answer is 2

As seen in Line 4, in order to store both values returned by divide() into variables in the main() method, the syntax is (variable1, variable2) = methodCall(parameters); When calling any method that returns multiple values, it’s necessary to use parentheses to group the variables where you’re storing the results, using the same syntax discussed in an earlier tutorial for sequences. Note that in Line 5, we left out the second return value. It’s perfectly acceptable to do this if you don’t need one or more of the return values. Simply leave a blank space for whichever value you’re choosing to ignore.

Note

The order of the values being returned must match the order of the variables you are assigning these values into. For example, if the first return value of a method is a String and the second is a double, putting a double variable first instead of a String variable will cause a compiler error.

A note on scope

The last topic we will discuss about methods is scope. The term scope was first defined in an earlier tutorial and determines the part of a program where a variable has meaning. For example, a counter variable declared inside of a for loop has scope only inside the loop itself. It cannot be accessed outside of the loop.

The same concept applies to method parameters.

Let’s say we have a method called doCoolStuff(), and in the main() method we have two variables and call a method:

String word = "pecan pie";
double number = 3.14;
String result = doCoolStuff(word, number);

The method header for doCoolStuff() is:

public doCoolStuff(String word, double number) => (String)

Are the parameters word and number the same as the variables word and number in the main method? No, even though they have the same names. Parameters are passed by value. In other words, if you change the value of word in doCoolStuff() to "apple pie", the variable word in the main() method will still equal "pecan pie". The parameters act as local variables whose scope is within the method where they are defined.

Whenever a method is called, the values of the parameters are copied from the calling code into the method. Thus, changing those parameters will never change the variables in the original code. This principle becomes more complicated when dealing with reference variables such as arrays and objects. Although it’s still true that you can’t change the variables themselves (pointing them at different references), it is often possible to change the values inside the arrays or objects that these variables point at. Since a copy of a reference variable still points at the same object, changes made to the contents of the arrays or objects will be reflected in the local variables of the calling code.