The Science of Programming/EncapsulatingOperations

Encapsulating Sequences of Operations

edit

Defining Functions

edit

Recall, the series of expressions we evaluated to find the y-value of a point on the line

 

given an x-value:

sway> var m = 6;
    INTEGER: 6

    sway> var x = 9;  
    INTEGER: 9

    sway> var b = -12;
    INTEGER: -12

    sway> var y = m * x + b;
    INTEGER: 42

    sway> y;
    INTEGER: 42

Now, suppose we wished to find the y-value corresponding to a different x-value or, worse yet, for a different x-value on a different line. All the work we did would have to be repeated. A function is a way to encapsulate all these operations so we can repeat them with a minimum of effort.

First, we will define a not too useful function that calculates y give a slope of 6, a y-intercept of -12, and an x-value of 9 (exactly as above).

sway> function findY()
    more>    {
    more>    var slope = 6;
    more>    var x = 9;
    more>    var intercept = -12;
    more>    return slope * x + intercept;
    more>    }
    FUNCTION: <function findY()>

Notice that the interpreter prompt changes to more when it the input is incomplete. In the case of the function definition above, that occurs when the curly close brace is entered (1).

Once the function is defined, we can find the value of y repeatedly:

sway> findY()
    INTEGER: 42

    sway> findY()
    INTEGER: 42

This function is not too useful in that we cannot use it to compute similar things, such as the y-value for a different value of x. A hallmark of a good function is that is lets you compute more than one thing. We can modify our function to take in the value of x we are interested in. We do this by passing in an argument, in this case, the value of x.

sway> function findY(x)
    more>    {
    more>    var slope = 6;
    more>    var intercept = -12;
    more>    return slope * x + intercept;
    more>    }
    FUNCTION: <function findY()>

We give names to the values being passed in by placing variable names between the function definition parentheses. In this case, we chose x as the name. Notice that since we are passing in x, we no longer need (or want) the definition of x, so we delete it. Now we can compute y for an infinite number of x's.

sway> findY(9);
    INTEGER: 42

    sway> findY(0);
    INTEGER: -12

    sway> findY(-2);
    INTEGER: -24

What if we wish to compute a y-value for a given x for a different line? One approach would be to pass in the slope and intercept as well as x:

sway> function findY(x,slope,intercept)
    more>    {
    more>    return slope * x + intercept;
    more>    }
    FUNCTION: <function findY()>

    sway> findY(9,6,-12);
    INTEGER: 42
    sway> findY(0,6,-12);
    INTEGER: -12

If we wish to calculate using a different line, we just pass in the new slope and intercept along with our value of x. This certainly works as intended, but is not the best way. One problem is that we keep on having to type in the slope and intercept even if we are computing y-values on the same line. Anytime you find yourself doing the same thing over and over, be assured that someone has thought of a way to avoid that particular tedium. So assuming that is true, how do we customize our function so that we only have to enter the slope and intercept once per particular line? We will explore three different ways for doing this. In reading further, it is not important if you understand all that is going on. What is important is that you know other approaches exist and understand the pros and cons of each approach:

Creating functions on the fly

edit

At this point, you should see that you can create functions and should be able to create other functions through pattern matching. For example, you ought to be able to define a function that squares a given number:

function square(x)
        {
        return x * x;
        }

Since creating functions is hard work (lots of typing) and Computer Scientists avoid hard work like the plague, somebody early on got the idea of writing a function that itself creates functions! Brilliant! We can do this for our line problem. We will tell our creative function to create a findY function for a particular slope and intercept!

function makeFinder(slope,intercept)
         {
         function findY(x)
             {
             return slope * x + intercept;
             }
        return findY;
        }

See how our creative function defines a findY function and then returns it. Now we can create a bunch of different findY's.

sway> var findYa = makeFinder(6,-12);
    FUNCTION: <function findY(x)>

    sway> var findYb = makeFinder(5,2);
    FUNCTION: <function findY(x)>

    sway> findYa(9);
    INTEGER: 42

    sway> findYb(9);
    INTEGER: 47

Notice how findYa and findYb remember the slope and intercept supplied when they were create. While this is decidedly cool, the problem is many languages (C and Java included) do not allow you to define functions that create other functions.

Using objects

edit

Another approach to our line problem is to use something called an object. In Sway, an object is simply an environment and we have seen those before. So there is nothing new here except in how to use objects to achieve our goal. Here, we define a creating function (known as a constructor) that creates and returns a line object.

function line(slope,intercept)
        {
        return this;
        }

The this variable always points to the current environment, which, in this case, includes slope and intercept. By returning this, we return this environment, and we can look up the values of slope and intercept at our leisure. To prove that slope and intercept exist, we can use the built-in bindings function:

sway> lineA = line(6,-12);
    sway> bindings(lineA);
    OBJECT 231:
        context : <object 145>
        dynamicContext: <object 145>
        constructor: <function line(slope,intercept)>
        this: <object 231>
        intercept: -12
        slope : 6
    SYMBOL: :true

We access the variables in an object with the '.' (dot) operator:

sway> lineA . slope;
    INTEGER: 6

    sway> lineA . constructor;
    FUNCTION: <function line(slope,intercept)>

Now we modify our findY function to take in a line object as well as x and use the dot operator to extract the line's slope and intercept:

function findY(line,x)
        {
        return line . slope * x + line . intercept;
        }

In this scenario, we create different lines, then pass each line to our new findY function:

sway> var lineA = line(6,-12);
    OBJECT: <object 231>

    sway> var lineB = line(5,2);
    OBJECT: <object 256>

    sway> findY(lineA,9);
    INTEGER: 42

    sway> findY(lineB,9);
    INTEGER: 47

The problem with this approach is we have separated line objects from finding y values, yet these two concepts are closely related. As an example, suppose we have parabola objects as well as line objects. Our findY function would fail miserably for parabola objects even though the concept of (x,y) points on a parabola is just as valid as points on a line (2). In the object oriented world, we solve this problem by bundling the object and functions that work specifically on that object together. In our case, we make the findY function part of the line object:

function line(slope,intercept)
        {
        function findY(x)
            {
            return slope * x + intercept;
            }
        this;
        }

This is very similar to the functions-on-the-fly approach, but we return this instead of findY. Now we call the findY function via the line object.

sway> var lineA = line(6,-12);
    OBJECT: <object 231>

    sway> var lineB = line(5,2);
    OBJECT: <object 256>

    sway> lineA . findY(9);
    INTEGER: 42

    sway> lineB . findY(9);
    INTEGER: 47

Should we have a parabola object, it would have its own findY function with a different implementation. We would call it just the same, however:

sway> var parabolaA = parabola(2,0,0);
    OBJECT: <object 453>

    sway> parabolaA . findY(7);
    INTEGER: 49

This approach is supported in object oriented languages such as Java. The earlier approach (where the function was separated from the object) is supported in procedural languages such as C.

Questions

edit

Exercises

edit

(1) Typing a long construct into the interpreter is tedious. Later we will learn how to store our code in a file and have the interpreter execute the code in that file. If we need to make changes to our code, we simply edit the file. That way, we do not need to type in the modified code into the interpreter from scratch.

(2) Here is a concrete example of trying to generalize, so that our function works for all objects for which the concept of the function is valid.