Functions, continued

← back to Creative Computing

Parameters

When we discussed functions before, the functions we wrote were intended to make certain sequences of statements reusable. There’s an additional feature of functions that lets you abstract code even further, by making it possible to “pass” values into a function from the code that calls it.

To illustrate, let’s return to the creepy face examples from earlier:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  translate(mouseX, mouseY);
  makeFace();
  pop();
}

function makeFace() {
  // draw a face, centered on the origin
  ellipse(-50, -25, 40, 40);
  ellipse(50, -25, 40, 40);
  arc(0, 25, 100, 50, 0, PI);
}

The makeFace() function draws a face centered around the origin. But let’s say that we want to introduce variations to the makeFace() function, like making it so the face has three or four eyes instead of two. We could, of course, write more functions to implement this functionality:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  translate(mouseX, mouseY);
  if (mouseIsPressed) {
    makeFaceFourEyes();
  }
  else {
    makeFaceThreeEyes();
  }
  pop();
}

function makeFaceThreeEyes() {
  // draw a face, centered on the origin
  for (let i = 0; i < 3; i++) {
    ellipse(-50 + (i*50), -25, 25, 25);
  }
  arc(0, 25, 100, 50, 0, PI);
}

function makeFaceFourEyes() {
  // draw a face, centered on the origin
  for (let i = 0; i < 4; i++) {
    ellipse(-50 + (i*33), -25, 25, 25);
  }
  arc(0, 25, 100, 50, 0, PI);
}

Your “unnecessary repetition” sense should be tingling. It would be nice to be able to make it so we could write one function to make a face, and then provide some extra information when we call the function, to give the function some information about how we want the face to be drawn—i.e., how many eyes to draw.

The easiest way to do this in Javascript is with parameters. When you write a function definition, you can put a comma-separated list of variable names inside of the parentheses. When the function is called, any values you supply inside of the parentheses in the function call will be available inside of your function as the variable names that you provided. So, for example, the following (fairly useless) function:

function drawCircle(x, y, radius) {
    ellipse(x, y, radius, radius);
}

… can be called like this:

drawCircle(10, 20, 30);

The value 10 will be available inside the function as x, the value 20 will be available as y, and the value 30 will be available as radius.

Here’s a revised version of the sketch above, with a makeFaceWithEyes() function that takes a parameter to specify how many eyes to draw. (Click to increase the number of eyes.)

► run sketch ◼ stop sketch
let clickCount = 0;
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  translate(mouseX, mouseY);
  // +2 to ensure we always have at least two eyes
  makeFaceWithEyes((clickCount % 8) + 2);
  pop();
}

function mousePressed() {
  clickCount++;
}

function makeFaceWithEyes(eyeCount) {
  let eyeDistance = 100.0 / (eyeCount - 1);
  for (let i = 0; i < eyeCount; i++) {
    ellipse(-50 + (i*eyeDistance), -25, 25, 25);
  }
  arc(0, 25, 100, 50, 0, PI);
}

EXERCISE: Modify the makeFaceWithEyes() function so that it doesn’t break if you pass 0 or 1 for the number of eyes. Then, modify the function so that you can specify how broad you want the smile to be.

Return values

We’ve seen some functions so far whose purpose is not to collect statements and execute them later, but to compute some value and then return the value. An example of this is sin(), which calculates the sine of a particular number. The random() function is another example of a function that doesn’t draw anything to the screen; its sole purpose is to do some calculation behind the scenes, and then evaluate to another value. (See the p5.js Math reference for more examples.)

You can write your own functions that operate this. If a function has a return value, then any call to the function evaluates to whatever value follows the return keyword inside of the function. For example, here’s a function that takes an array as a parameter, and returns the sum of all of the items in the array (assuming they’re all numbers):

function sum(t) {
    let result = 0;
    for (let i = 0; i < t.length; i++) {
        result += t[i];
    }
    return result;
}

Here’s an example of the function in use:

let myNumbers = [5, 6, 7, 8, 9];
console.log(sum(myNumbers)); // prints 35

Let’s return to the multiple-eyed face in the previous section. One problem with the code as written is that the eyes are always the same size, regardless of how many eyes there are. It might be nice to make the eyes change size dynamically, so that faces with only a few eyes have bigger eyes than faces with more. To accomplish this, we might write a function like so:

function getEyeSize(count) {
    return 50 - (count * 5);
}

We’ll then call the function from inside the makeFaceWithEyes() function, passing along the number of eyes desired by the original caller. Here’s the full example:

► run sketch ◼ stop sketch
let clickCount = 0;
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  translate(mouseX, mouseY);
  // +2 to ensure we always have at least two eyes
  makeFaceWithEyes((clickCount % 8) + 2);
  pop();
}

function mousePressed() {
  clickCount++;
}

function getEyeSize(count) {
    return 50 - (count * 5);
}

function makeFaceWithEyes(eyeCount) {
  let eyeDistance = 100.0 / (eyeCount - 1);
  for (let i = 0; i < eyeCount; i++) {
    ellipse(-50 + (i*eyeDistance), -25,
        getEyeSize(eyeCount),
        getEyeSize(eyeCount));
  }
  arc(0, 25, 100, 50, 0, PI);
}

Functions as values

Functions in Javascript are actually another kind of value, just like numbers, arrays and objects. It doesn’t look like it in the functions we’ve written so far, but making a function like this:

function sum(a, b) {
  return a + b;
}

… is actually (more or less) just shorthand for this:

let sum = function(a, b) { 
  return a + b;
}

These are both valid ways to declare a function, but the second method makes it more plain what is happening behind the scenes: we’re creating a new variable, sum, and assigning to it a function value. If you’ve defined a function with the second syntax, you call it just as if you’d defined it with the first syntax, like so:

console.log(sum(1, 7)); // prints 8

Because functions are values, we can assign them to variables, put them in arrays or objects, or pass them as parameters to other functions. (Whoa.) To demonstrate, let’s start with this sketch, which defines three functions to draw simple shapes to the screen:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
  push();
  translate(100, 200);
  drawSquare();
  pop();
  push();
  translate(150, 200);
  drawCircle();
  pop();
  push();
  translate(200, 200);
  drawTriangle();
  pop();
  push();
  translate(250, 200);
  drawCircle();
  pop();
  push();
  translate(300, 200);
  drawSquare();
  pop();
}
function drawSquare() {
  rectMode(CENTER);
  rect(0, 0, 40, 40);
}
function drawCircle(diameter) {
  ellipse(0, 0, 40, 40);
}
function drawTriangle() {
  triangle(0, -20, 20, 20, -20, 20);
}

This is nice, but there’s some repetition that we could factor out here. We’re setting the translation values manually for each shape. Ideally, we’d find some way to write a loop to accomplish this task. But up to this point, we don’t have a good abstraction for this purpose, other than to check the loop variable at each step—something like this, maybe:

for (let i = 0; i < 5; i++) {
  push();
  translate(100 + (i*50), 200);
  if (i == 0 || i == 4) {
    drawSquare();
  }
  else if (i == 1 || i == 3) {
    drawCircle();
  }
  else if (i == 2) {
    drawTriangle();
  }
  pop();
}

This would work, but it’s fairly brittle. If we want to change the order of the shapes, we’d need to change the logic inside of the for loop. There’s an easier way, which is to make an array of the functions we want to call, like so:

► run sketch ◼ stop sketch
let callOrder = [
  drawSquare,
  drawCircle,
  drawTriangle,
  drawCircle,
  drawSquare];

function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
  for (let i = 0; i < callOrder.length; i++) {
    push();
    translate(100 + (i*50), 200);
    callOrder[i]();
    pop();
  }
}
function drawSquare() {
  rectMode(CENTER);
  rect(0, 0, 40, 40);
}
function drawCircle(diameter) {
  ellipse(0, 0, 40, 40);
}
function drawTriangle() {
  triangle(0, -20, 20, 20, -20, 20);
}

Pretty cool, right? The callOrder array determines what order the functions are called in. (Try rearranging the values, or adding new values to the end.) Notice that to refer to the function value, you use the name of the function without any parentheses following.

But wait a second, what’s this weird thing?

callOrder[i]();

Looks like a bunch of nonsense if you ask me. But it’s actually fairly simple to explain. This expression:

callOrder[i]

… evaluates to the *function stored at the ith index of the callOrder array. To *call* a function, you write parentheses (()`) right after the expression that evaluates to the function value. So

callOrder[i]();

calls the function stored at the ith index of the array.

The following example retains the three drawing functions above, but adds a new function, randomlyDraw(), which takes a list of functions and calls one of the functions at random:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
}
function draw() {
  randomlyDraw([drawSquare, drawCircle,
    drawTriangle]);
}
function drawSquare() {
  rectMode(CENTER);
  rect(0, 0, 40, 40);
}
function drawCircle(diameter) {
  ellipse(0, 0, 40, 40);
}
function drawTriangle() {
  triangle(0, -20, 20, 20, -20, 20);
}
function randomlyDraw(funcs) {
  let choice = floor(random(0, funcs.length));
  push();
  stroke(random(255));
  translate(random(width), random(height));
  rotate(random(PI*2));
  scale(random(5));
  funcs[choice]();
  pop();
}

EXERCISE: Write a function that takes two parameters: a function and a number. The function you write should call the function provided in the parameters multiple times, as specified by the numeric parameter.

Further reading