Learning the C Programming Language as a Classical Musician [18]

Episode 18 — Types (Part 10: Function types)

Welcome back!

Today we are scratching the surface of one of the greatest subjects in programming: functions! My goal in this episode will be to try to keep this concise and, at the same time, interesting and helpful to read.

Let’s dive right in!

Function types

It is often said that computers were invented to save time, to perform calculations and operations that humans would have taken ages to complete manually. This concept comes from the synthesis quality a machine has when it comes to repeating the same action multiple times, or to condense in a single line of code (such as when we #import a header file) possibly thousands of lines of code. To accomplish this, functions play a crucial role, especially when it comes to flexibility. Imagine, in classical music, a piece in Rondò form, which sees a part A come back after several episodes, effectively creating an A-B-A-C-A form, with two contrasting B and C episodes, and the “rondò” part coming back in between and at the end. Already at the dawn of this musical form, during the XVII century, the issues of printing, ink, and manual labour cost made composers and publishers alike think about a possible solution. After part B they would write “now play part A, then go straight to C”; after part C they would write “part A again”. Of course, they would use symbols to abbreviate even that, but this meant that, instead of having to write five parts (in case of an A-B-A-C-A form), they would need only three (A-B-C), basically halving the paper, ink, and time cost.

If we wanted to write a Rondò piece in C we would need to have an enumeration of pitches, an enumeration, or a struct of rhythms, combine them together in separate objects and then list them. Even in the simplest form, this could turn out to be thousands of lines of code. Here is an extremely simplified example:

enum pitches { C, D, E, F, G, A, B };
enum rhythms { whole, half, quarter, eight, sixteenth, thirtysecond };
    
struct note {
    enum pitches p;
    enum rhythms r;
};
    
struct note first;
first.p = C;
first.r = whole;

Repeat this process for every note of part A, and you can already look at something pretty big, and this assumes this is a solo piece! Subsequently, we would repeat the process for part B but, at the comeback of part A, instead of writing again everything, not even copy-pasting, we could condense all part A in a construct called function and just call it back. Let me clarify something: I would certainly not write all this inside the main function, rather I would create separate files for every single aspect of this, possibly ending up with a big object (possibly a struct, or a combination of different types) called partA, which would then be called all together using a function returning it as a type. As it often happens to me, I tend to choose examples that are, by definition, difficult to realise in practice, but I hope this will clarify what I want to show:

struct musicalPart {
    // This struct contains every instruction to generate our music
    // It has a call to printf at the end to print all notes in the correct order
};
    
struct musicalPart part_A;
struct musicalPart part_B;
struct musicalPart part_C;

Then I would define a function that would return a string (in this case an array of characters) containing the relevant information:

char * playPartA() {
    printf("Here are the notes for part A:\n");
    // here I would call the relevant part of the struct and return it
    return "Placeholder return text";
};

The syntax is important here: first we have a return type, showing what the function is giving back as a result, then a name (identifier) followed by a pair of parentheses (more on this in a bit). Then, inside the braces, we would write everything we want our function to do, and end up with a return statement. Instead of char * in the beginning, we could have written void to show we didn’t need anything returned, and we just cared about what the function actually did, possibly a better choice for our example. So, change the function by replacing char * with void and replacing the return statement with another printf call. Eventually, we would just call the function like this inside of our code:

playPartA();

The reason this is great is that we would not need to rewrite every single line of code that made up Part A.

I am sure you may be slightly confused at this point, and I would not blame you, so let’s take a step back while keeping in mind that functions are basically blocs of reusable code. We are now going to look at how to use them to their best.

Function declaration and definition

By definition, a function is a C language construct that associates a compound statement,1 the function body, with an identifier, the function name. We have actually had functions under our nose all the time, since every C program begins execution from the main function, which has the task of invoking other functions, either user-defined or library imported.

int main(int argc, const char * argv[]) {
	return 0;
}

While this is not exactly simple as a function example, let’s break it down:

  • int means that this function returns an integer. In the function’s body we see the line return 0;, as a function that declares to be returning something, needs to do so.
  • main is the function’s identifier, its name.
  • The identifier is followed by a pair of brackets () which can be empty if the function does not accept any parameter (that is, any option you may want to give your function to make it reusable), or populated by a comma-separated list of statements. In this case, we have an integer called argc, and a constant characters array called argv. Do not worry about what they actually do right now.
  • The open brace { represents the start of the function’s body, which will end at the last close brace }.
  • The return keyword precedes the return statement, which is necessary only if the function does not start with void.

In the C Programming Language, functions needs to be declared and then defined. Declaration can appear at any scope, while definition is usually added after the main function. Here’s an example:

// This is a function declaration, inside the main function
void printTimeSignature(int above, int below);

// This is the function definition, outside of the main function
void printTimeSignature(int above, int below) {
    printf("This piece is in %d/%d.\n", above, below);
}
    
// Back into main, we call it, passing 'arguments' for its parameters
printTimeSignature(3, 4);

Remember this: parameters are used in the function’s declaration and definition phase, while when we call the function, we pass arguments. This may seem unimportant now, but in Swift, one will be able to give a parameter name and an argument name, to make code more expressive and easy to read.

Declaration

We saw how a function’s declaration may appear at any scope. This is also called the function’s prototype, as it is basically useless by itself, but gives us a hint of what to expect. When we declare a function, we choose a declarator, which is composed by a return type and an identifier (a name), and an optional list of parameter types. The return type can be void (i.e., nothing, meaning this function returns no value), but cannot be a pointer. The parameter list can either be void, or a comma-separated list of parameters, which may end with the ellipsis parameter ... (we have encountered this in the printf function, for example).

During the different revisions the C programming language has gone through (mainly C89, C99, C11, C17, and the upcoming C23), the syntax for function declaration has slightly evolved. Do not be surprised if Xcode sometimes throws a warning about the way you declare your functions. You need to be flexible and adapt to the current needs of the compiler.

For example, sometimes we get parameter names, and sometimes we do not. In the beginning, functions could even just have parameter names but no parameter type! This meant that one could declare a function like this:

int myFunc(a, b);

Xcode would now warn us about this, saying “A parameter list without types is only allowed in a function definition”. To avoid useless troubles, please just always name your parameters, and give them appropriate types. Your life will be just so much easier!

We could analyse every example the documentation has to offer on function declarations but, sincerely, most of them are not immersed in a practical environment, and I would prefer to get to real-word examples as soon as possible.

Definition

First fundamental thing to remember: function definitions are allowed at file scope only, as nested functions cannot exist. This means that defining a function inside the main function would return the error “Function definition is not allowed here”. Since my early contacts with C in the CS50 course by Harvard University, I learned that the most elegant way to write functions is to write the prototype (declaration) before the main function, but after the #include statements, and the definition after main’s closing brace.

A function’s definition is generally composed by the same declarator found in its declaration, its identifier, its parameter list, if present, otherwise (void), and the function-body. This last one is contained within a pair of braces and gets executed every time the function gets called. For example, let’s create a function that tells us what kind of piece we are playing, tempo-wise. I know in music this is up for some debate as metronomic marking is not necessarily meaning anything precise, but, for starters, it will work.

void printTempo(int mm) {
    char * tempo;
    if (mm >= 144) {
        tempo = "a Presto\n";
    } else if (mm >= 120 && mm < 144) {
        tempo = "an Allegro\n";
    } else if (mm >= 100 && mm < 120) {
        tempo = "a Moderato\n";
    } else if (mm >= 80 && mm < 100) {
        tempo = "an Andante\n";
    } else if (mm >= 60 && mm < 80) {
        tempo = "an Adagio\n";
    } else if (mm >= 40 && mm < 60) {
        tempo = "a Largo\n";
    } else {
        tempo = "too slow to bother!\n";
    }
    printf("This movement is %s", tempo);
//    printf("This movement is an %s", (mm >= 120) ? "Allegro" : "Moderato");
}

We would then call it like so:

printTempo(83);

Which will print:

This movement is an Andante

Few extra details before closing up this episode. Inside a function’s body, every named parameter is a lvalue expression (left-value, something found to the left of the equal sign and that, thus, can be assigned some value). They have an automatic storage duration (meaning that the program will decide when they will no longer be needed, freeing up memory as a result), and block scope (how much I love this subject!).

In the last function printTempo we could not use the range operators we can find in Swift, but we could use macros to simulate them. Macros are shortcuts that allow us to store global values to be used elsewhere, such as:

#define kTuning = 442

Notice how this doesn’t require the semicolon at the end of the line. The #define directive asks the program to substitute every instance of kTuning with 442. The advantage of this is that if our program gets any bigger, we won’t have to change the value manually in every place we called it in, rather only at the top of the file.

This could then be called as:

printf("Tonight we are playing in %d\n", kTuning);

Getting back to range operators, in Xcode, we have to resort to macro usage. Macros can also have parameters, so we could create something like this:

#define OPENRANGE(value, low, high) (((value) >= (low)) && ((value) <= (high)))
#define CLOSEDRANGE(value, low, high) (((value) >= (low)) && ((value) < (high)))

The above code could then be substituted with this, much neater code:

void printTempo(int mm) {
    char * tempo;
    if (CLOSEDRANGE(mm, 144, 300)) tempo = "a Presto\n";
    else if (OPENRANGE(mm, 120, 144)) tempo = "an Allegro\n";
    else if (OPENRANGE(mm, 100, 120)) tempo = "a Moderato\n";
    else if (OPENRANGE(mm, 80, 100)) tempo = "an Andante\n";
    else if (OPENRANGE(mm, 60, 80)) tempo = "an Adagio\n";
    else if (OPENRANGE(mm, 40, 60)) tempo = "a Largo\n";
    else tempo = "too slow to bother!\n";
    printf("This movement is %s", tempo);
}

We have not really saved so much typing, but we are for sure writing safer code because we cannot mistype anything in the condition. Something else you may learn here is that, if the if statement has only one line of code, one can do away with braces.

I have eventually found that Xcode supports the GCC extensions to the C programming language, which actually enables open range operators. Our code above could be rewritten like so now:

void switchTempo(int mm) {
    char * tempo;
    switch (mm) {
        case 144 ... 300: tempo = "a Presto\n";
            break;
        case 120 ... 143: tempo = "an Allegro\n";
            break;
        case 100 ... 119: tempo = "a Moderato\n";
            break;
        case 80 ... 99: tempo = "an Andante\n";
            break;
        case 60 ... 79: tempo = "an Adagio\n";
            break;
        case 40 ... 59: tempo = "a Largo\n";
            break;
            
        default: tempo = "too slow to bother!\n";
            break;
    }
    printf("This movement is %s", tempo);
}

Which one is better? The one you prefer! All of them work!

What’s next?

This was a quite deep introduction to functions, and I promise we will get back with more examples soon. We only have two subjects left in our journey around C types: pointer types, and atomic types. I am sure you will find pointers fascinating! Until the next one, thank you!

Bottom Line

Thank you for reading today’s article.

If you have any question or suggestion, please leave a comment below or contact me using the dedicated contact form. Assuming you do not already do so, please subscribe to my newsletter on Gumroad, to receive exclusive discounts and free products.

I hope you found this article helpful, if you did, please like it and share it with your friends and peers. Don’t forget to follow me on this blog and to please let me know what you think.

If you are interested in my music engraving services and publications don’t forget to visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s Swift programming books from this Affiliate Link or BigMountainStudio’s books from this Affiliate Link.

Thank you so much for reading!

Until the next one, this is Michele, the Music Designer.

  1. Also known as a block, it is a sequence of statements and declarations enclosed within braces { }.

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a comment