CS 136 - 2 - Imperative C
2.1 - I/O and output side effects
Functional Programming
A programming paradigm is the programming approach, philosophy or style
In CS135, Racket uses a purely functional programming paradigm.
- Functions are pure (mathematical)
- functions only return values
- return values only depend on argument values
- Only constants are used
Imperative Programming
- This course uses imperative programming paradigm
- Functions may be impure
- Variables and constants are used
- A sequence of instructions (statements) are executed
- A block
{}is formally known as a compound statement (a sequence of statements) - Control flow is used to change the order of statements
- Side effects are used - this is the biggest difference between functional and imperative programming
Blocks may contain local variable definitions
See:
// This program demonstrates a compound statement
#include "cs136.h"
int main(void) {
trace_int(1 + 1); // do this first
assert(3 > 2); // then do this
return 0; // and then do this
}
I/O
Input/Output is the term used to describe how a program interacts with the real world.
- In this course, we only use simple text-based I/O
Text I/O
To display text output in C, we use the printf function with a "string" paramter.
- Note: The newline character (
\n) is necessary to properly format output to appear on multiple lines.
See below:
// This program demonstrates output
#include "cs136.h"
int main(void) {
printf("Hello, World");
printf("C is fun!");
printf("Hello, World\n");
printf("C is\nfun!\n");
}
- Format specifiers (the f in printf) can be used to format a string. In our case, we'll use
%d - To print a percent sign (%), you use two (%%)
- To print a backslash (\), you use two (\\)
- To print a quote ("), add an extra backslash (\")
See below for an example:
// This program demonstrates format specifiers
#include "cs136.h"
int main(void) {
printf("2 plus 2 is: %d\n", 2 + 2);
printf("%d plus %d is: %d\n", 2, 10 / 5, 2 + 2);
printf("I am %d%% sure you should watch your", 100);
printf("spacing!\n");
printf("4 digits with zero padding: %04d\n", 42); // format and alignment
}
Code output:
2 plus 2 is: 4
2 plus 2 is: 4
I am 100% sure you should watch yourspacing!
4 digits with zero padding: 0042
Side effects and state (introduction)
- If a function produces output, then it has a side effect
- In general, a side effect is when the state of something "changes". State refers to the value of some data at a moment in time
Consider the following “real world” example: You have a blank piece of paper, and then you write your name on that paper. You have changed the state of that paper: at one moment it was blank, and in the next it was “autographed”. In other words, the side effect of writing your name was that you changed the state of the paper.
Documenting Side Effects
printffunction has a side effect - it changes the output (display)- Always add an effects: section to document any side effects
- IF YOU DON'T YOU WILL LOSE MARKS!
- If the side effect occurs conditionally, add the word "may"
// This program demonstrates how to DOCUMENT
// printf (output) side effects
#include "cs136.h"
// sqr(n) computers n^2
int sqr(int n) {
return n * n;
}
// noisy_sqr(n) computes n^2
// effects: produces output
int noisy_sqr(int n) {
printf("Yo! I'm squaring %d!\n", n);
return n * n;
}
// noisy_abs(n) computes |n|
// effects: may produce output
int noisy_abs(int n) {
if (n < 0) {
printf("Yo! I'm changin' the sign!\n");
return -n;
} else {
return n;
}
}
int main(void) {
trace_int(noisy_sqr(-3));
trace_int(noisy_abs(3));
trace_int(noisy_abs(-3));
assert(sqr(-3) == 9);
assert(sqr(7) == 49);
assert(noisy_sqr(-3) == 9);
assert(noisy_sqr(7) == 49);
}
Code Output:
Yo! I'm squaring -3!
Yo! I'm changin' the sign!
Yo! I'm squaring -3!
Yo! I'm squaring 7!
>>> [main.c|main|30] >> noisy_sqr(-3) => 9
>>> [main.c|main|31] >> noisy_abs(3) => 3
>>> [main.c|main|32] >> noisy_abs(-3) => 3
I / O Terminology
- Output is different than return
- Passing is different than inputting
Debugging Tools
assert,trace_int, etc. are not considered side effects. Do not documentassert's or tracing in the "effects" section.
void functions
- It is possible a function has a side effect and returns a value
- However, a function may have a side effect but not return a value
Expression Statements
- an expression statement is an expression with a semicolon (;). The value of an expression statement is discarded after its executed
- The purpose of an expression statement is to generate side effects. This is why, imperative programming is, in essence, "programming by side effects"
Statement Types
- There are three types of C statements:
- compound statements (blocks)
{}a sequence of statements (to be executed in order) - expression statements for generating side effects (values are discarded)
- control flow statements control the order in which other statements are executed (e.g.,
return,if, andelse)
- compound statements (blocks)
// This program demonstrates unusual expression statements
#include "cs136.h"
// display_score(score, max) displays the player score
// effects: produces output
void display_score(int score, int max) {
printf("your score is %d out of %d.\n", score, max);
return; // optional
}
// sqr(n) computes n^2
int sqr(int n) {
return n * n;
}
int main(void) {
display_score(97, 100);
11; // throws an edX warning
10 + 1; // throws an edX warning
sqr(6) - sqr(5); // throws an edX warning
printf("expression\n");
printf("five\n") + 6; // throws an edX warning
// Tracing unusual expression statements
trace_int(11);
trace_int(10 + 1);
trace_int(sqr(6) - sqr(5));
trace_int(printf("expression\n"));
trace_int(printf("five\n") + 6);
}
2.2 - Mutation side effects
More side effects
Two more types:
- Input
- Mutation (modifying variables)
Variables
- Variables store values
- To define a variable, we need (in order):
- the type
- the identifier or name
- We should also have the initial value as it's considered bad style in this course to declare a variable but not define it
- When we set an initial value, we are initializing the variable. Variable definitions are not considered to be statements.
See below for an example:
// My first program with mutation
#include "cs136.h"
int main(void) {
int m = 5; // definition (with initialization)
trace_int(m);
m = m + 1; // mutation!
trace_int(m);
m = -1; // more mutation!
trace_int(m);
}
Code output:
>>> [main.c|main|7] >> m => 5
>>> [main.c|main|9] >> m => 6
>>> [main.c|main|11] >> m => -1
Mutation
- When the value of a variable changes, it's called mutation
- Mutation is a side effect because when mutation occurs, the "state" (value) of the variable changes
Assignment Operator
- Mutation is achieved through the assignment operator (
=) - To accomplish:
- The right hand side must be an expression that produces a value with the same type as the left hand side
- The LHS must be the name of a variable (for now)
- The LHS variable is changed (mutated) to store the value of the RHS expression
- This is a side effect - the state of the variable has changed
- (
1)x = y = z = 0;is the same as(2)(x = (y = (z = 0))); - Avoid (
1), it's bad style! Do (2) - Remember to always use a double
==for equality, not a single=!
Example:
// Demonstrating using = instead of == for equality
#include "cs136.h"
int main(void) {
int i = 0;
if ((i = 13)) {
printf("disaster!\n");
}
trace_int(i);
}
Code output:
disaster!
>>> [main.c|main|12] >> i => 13
- The reason for this is that it sets
ito be13. Anything that is not0istrue, and so theifcondition istrueand it prints "disaster!"
Initialization
- Always initialize variables!!!
- It's not too important for right now, but keep in mind that the
=used in initialization is not the same as the assignment operator.
Example:
// Demonstrating initialization
#include "cs136.h"
int main(void) {
int my_variable = 7; // initialized
int another_variable; // uninitialized (BAD!)
int n = 5; // initialization syntax
n = 6; // assignment operator
int x = 0, y = 2, z = 3; // bad style
int a, b = 0; // a is uninitialized (BAD!)
}
More assignment operators
+=combines the addition and assignment operators (for convenience)-=also exists, so does*=,/=,%=- There are also increment and decrement operators that increase or decrease a variable by one
- If you follow the "one side effect per expression" rule, it doesn't matter if you use postfix or prefix
- The prefix (
++x) and postfix (x++) have different precedences within the order of operationsx++produces the "old" value ofxand then incrementsx, whereas++xincrementsxand then produces the "new" value ofx.- Prefix is usually preferred for clarity
Example:
- The prefix (
// Demonstrating assignment operators
#include "cs136.h"
int main(void) {
int x = 0;
int j = 0;
x += 4; // x = x + 4;
printf("After line 8: x = %d\n", x);
x -= 2; // x = x - 2;
printf("After line 10: x = %d\n", x);
x++; // x += 1;
printf("After line 12: x = %d\n", x);
x--; // x -= 1;
printf("After line 14: x = %d\n", x);
x = 5;
j = x++; // j = 5, x = 6 (BAD STYLE!)
printf("After line 18: x = %d and j = %d\n", x, j);
x = 5;
j = ++x; // j = 6, x = 6 (BAD STYLE!)
printf("After line 22: x = %d and j = %d\n", x, j);
}
Code output:
After line 8: x = 4
After line 10: x = 2
After line 12: x = 3
After line 14: x = 2
After line 18: x = 6 and j = 5
After line 22: x = 6 and j = 6
Constants
- A constant is a variable that is immutable
- The value of a constant cannot be changed
- To define a constant, we add the
constkeyword to the type. - It is considered good style to use constants where appropriate because it:
- Communicates the intended use of the variable,
- Prevents 'accidental' or unintended mutation, and
- may help to optimize code
Global and local variables
- Variables are either global or local
- Global ones are defined outside functions (at the top level)
- Local ones are defined within functions
Example:
// Demonstrating constants and global/local variables and constants
#include "cs136.h"
const int my_global_constant = 42;
int my_global_variable = 7;
void f(void) {
const int my_local_constant = 22;
int my_local_variable = 11;
trace_int(my_local_constant);
trace_int(my_local_variable);
trace_int(my_global_variable);
}
int main(void) {
trace_int(my_global_constant);
trace_int(my_global_variable);
f();
}
2.3 - More Mutation
Variable Scope
- The scope of a variable is the region of code where it is "accessible" or "visible"
- For global variables, its scope is anywhere below its definition
Block (local) scope
- Local variables have block scope, meaning their scope extends from their definition to the end of the block they are defined in
- Variables with the same name can shadow other variables from outer scopes
- Modern programming guidelines recommend you define a variable:
- In the narrowest scope possible
- As close to its first use as possible
- This improves readability and ensures that when a variable is first used its type and initial value are accessible
Example:
// Demonstrating scope
#include "cs136.h"
void f(int n) {
// b OUT of scope
if (n > 0) {
// b OUT of scope
int b = 19;
// b IN scope
trace_int(b);
}
// b OUT of scope
// ...
}
// g OUT of scope
int g = 1;
// g IN scope
int main(void) {
// g IN scope
trace_int(g);
f(2);
// shadowing example:
trace_int(g); // g => 1
int g = 2;
trace_int(g); // g => 2
{
int g = 3;
trace_int(g); // g => 3
}
trace_int(g); // g => 2
}
Example:
// Discuss with your neighbour; figure out what the following code should display.
// Once you have written down your best guess, uncomment and run it.
#include "cs136.h"
/*
guess:
4 8
5 7
*/
/*
void squish(int lo, int hi) {
if (lo < hi) {
printf("%d %d\n", lo, hi);
return squish(++lo, hi--);
} else {
return;
}
}
*/
// this is much better !
void squish(int lo, int hi) {
if (lo < hi) {
printf("%d %d\n", lo, hi);
return squish(lo + 1, hi + 1);
} else {
return;
}
}
int main(void) {
squish(4, 8);
}
"Impure" functions
noisy_sqris "impure" because it has a side effect (produces output)
int noisy_sqr(int n) {
printf("Yo! I'm squaring %d!\n", n);
return n * n;
}
- "Impure" functions are sometimes called "procedures" or "routines" to distinguish their behaviour from "pure" functions
- Document any functions with mutation side effects
Mutating global variables
- A function that mutates a global variable has a mutation side effect which makes it "impure"
- You may not mutate global variables unless explicitly permitted by the course instructors
// Demonstrating mutating a global variable
#include "cs136.h"
int counter = 0; // global variable
// increment() returns the number of times it has been called
// effects: modifies counter
int increment(void) {
counter += 1;
return counter;
}
int main(void) {
assert(increment() == 1);
assert(increment() == 2);
}
Mutating local variables
- Mutating a local variable does not give a function a side effect since it does not affect state outside the function (global state)
For example:
// Demonstration: mutating a local var is NOT a function side effect
#include "cs136.h"
// add1(n) calculates n + 1
int add1(int n) {
int k = 0;
k += 1; // a local variable is being mutated (no side effect)
return n + k;
}
int main(void) {
assert(add1(3) == 4);
}
Mutating Parameters
- Parameters are nearly indistinguishable from local variables and may also be mutated
- Parameters are talked more about in later sections
Example (theadd1function produces no side effects):
- Parameters are talked more about in later sections
// Demonstration: mutating a parameter is NOT a function side effect
#include "cs136.h"
// add1(n) calculates n + 1
int add1(int n) {
n += 1;
return n;
}
int main(void) {
assert(add1(3) == 4);
}
Global dependency
- A "pure" function only depends on its argument values
- An "impure" function depends on a global mutable variable, even if the function produces no side effects!
Avoid global mutable variables
- They are almost always bad style and should definitely be avoided.
- You finna get a spanking
- We are NOT allowed to use global mutable variables on assignments
- However, global constants are AMAZING STYLE! Strongly encouraged.
Example:
// Demonstration: Functions with global dependency are "impure"
#include "cs136.h"
int n = 10;
// addn(k) returns n + k
int addn(int k) {
return k + n;
}
int main(void) {
assert(addn(5) == 15);
n = 100;
assert(addn(5) == 105);
}
- They have the scope of a local variable, but the duration of a global variable and their value persists between function calls.
- THEY ARE ALMOST ALWAYS POOR STYLE!
- Not allowed in CS136!!
- Example:
int increment(void) {
static int counter = 0;
counter += 1;
return counter;
}
More on scope
- Functions also have scope - by declaring a function, you bring the function in scope (you can use it now)
- Will discuss in later section
2.4 - Input side effects
Text input
- The
stdio.hC library also includes thescanffunction to read input!
How to use:
int x = 0;
int retval = scanf("%d",&x);
- First argument is a format specifier -
%dtellsscanfwe are expecting a decimal integer - Second argument is an expression -
&xtellsscanfwhere to store the value that is read- In C,
&is the syntax for the address of a variable. Saved for a later section
- In C,
- If
scanfreads an integer, the variable we use (in this casex) will store the read integer.- This means that
xhas been mutated!!
- This means that
- In CS136, only read in one value per
scanf
scanf return value
- The return value of
scanfis an integer and will be either:- the quantity (count) of values successfully read, or
- the constant EOF: the End Of File has been reached
- If input is not formatted properly,
0is returned. For example, if we input "hello"
In our environment, EOF is -1, however, it is good style to use the constant EOF instead of -1
Invalid input
- Always check the return value of
scanf- one is "success"
Example:
// This program demonstrates how to use scanf
#include "cs136.h"
int main(void) {
int val = 0;
int retval = 0;
retval = scanf("%d", &val);
if (1 == retval) {
printf("Read an int: %d\n", val);
} else {
printf("Failed to read: val is still %d\n", val);
printf("retval is %d\n", retval);
}
If on stdin, you input 4 5, code output will be:
Read an int: 4
- Notice:
retval = scanf("%d", &val)causes three things:- a value is read from input, a side effect
valis mutatedretvalis mutated
Reading many integers
// This program demonstrates recursively reading input
#include "cs136.h"
// print_forward reads from input and outputs each value
// (until a read failure occurs)
// effects: reads input
// produces output
void print_forward(void) {
int n = 0;
int retval = scanf("%d",&n);
if (retval != 1) {
return;
} else {
printf("%d\n", n);
print_forward();
}
}
int main(void) {
print_forward();
}
Another example - There is a switch in the occurrence of the printing and recursive call. Also, this is more concise:
// This program demonstrates recursively reading input
#include "cs136.h"
// print_reverse reads from input and outputs each value in
// reverse order (until a read failure occurs)
// effects: reads input
// produces output
void print_reverse(void) {
int n = 0;
if (scanf("%d",&n) != 1) {
return;
} else {
print_reverse();
printf("%d\n", n);
}
}
int main(void) {
print_reverse();
}
2.5 - I/O Testing
Input formatting
- When C reads an integer value using
scanf, it skips over whitespace (newlines and spaces)
Invalid input
- In this course, you do not have to worry about the instructors testing code with invalid input files
Testing harness
Out function testing strategies are:
- return values - use
assert's - input and output -
[Run with Tests]
There is another approach for return values. We can write a dedicated test function that reads in argument values from input, passes those values to the function, and prints out the return values. It is useful for both "pure" and "impure" functions.
Example:
// This program demonstrates an I/O testing harness
// (see test_sqr)
#include "cs136.h"
// sqr(n) computes n^2
int sqr(int n) {
return n * n;
}
// test_sqr() is an I/O testing harness for sqr
// it continuously reads in argument values (e.g., n)
// and then prints out sqr(n)
// effects: reads input
// produces output
void test_sqr(void) {
int n = 0;
if (scanf("%d", &n) == 1) {
printf("%d\n", sqr(n));
test_sqr(); // recurse
}
}
int main(void) {
test_sqr();
}
Enhancing the testing harness
- We can test different functions based on the input
For example:
main.c:
// This program demonstrates an I/O testing harness with symbols
#include "cs136.h"
// abs(n) returns the absolute value of n
// uses the ternary ? operator just to showcase it
int abs(int number) {
return (number < 0) ? -number : number;
}
// sqr(n) computes n^2
int sqr(int n) {
return n * n;
}
void run_test(void) {
const int CMD_ABS = lookup_symbol("ABS");
const int CMD_SQR = lookup_symbol("SQR");
const int CMD_SQUARE = lookup_symbol("SQUARE");
int cmd = read_symbol();
if (cmd != INVALID_SYMBOL){
int n = 0;
if (scanf("%d", &n) == 1){
if (cmd == CMD_ABS) {
printf("%d\n", abs(n));
} else if (cmd == CMD_SQR || cmd == CMD_SQUARE){
printf("%d\n", sqr(n));
}
run_test(); // run next test
}
}
}
int main(void) {
run_test();
}
test1.in:
ABS -1
ABS 2
SQR 5
SQUARE 9
test1.expect:
1
2
25
81
Symbol Tools Documentation (cs136.h library)
/****************************************************************************
SYMBOL TOOLS
****************************************************************************/
// symbols follow the same naming convention as identifiers ("names") in C:
// - they can only contain letters, underscores and numbers
// - they must start with a letter
// - they must be <= 63 characters
// at most there can be 255 symbols defined
// when reading or looking up symbols, they are assigned an int ID
// the constant INVALID_SYMBOL is returned by lookup_symbol & read_symbol when:
// a) the next symbol in the input or the parameter is invalid, or
// b) the end of the input (e.g., EOF) is encountered (read_symbol only), or
// c) a new symbol is being defined and 255 symbols have already been defined
extern const int INVALID_SYMBOL;
// read_symbol(void) returns the ID for the next valid symbol from input
// (which may be a new or existing ID) or INVALID_SYMBOL
// effects: reads from input
int read_symbol(void);
// lookup_symbol(symbol_string) returns the ID for symbol_string
// (which may be a new or existing ID) or INVALID_SYMBOL
int lookup_symbol(const char *symbol_string);
// print_symbol(symbol_id) displays the symbol corresponding to symbol_id
// requires: symbol_id is a valid ID
// effects: displays a message
void print_symbol(int symbol_id);
Testing Terminology
Assertion testing: using assertions to test our code
I/O-driven testing: uses input and expected output
Testing harness: using input to call functions
White box testing: you can see the code being tested
Black box testing: you can not see the code being tested
Unit testing: testing one piece at a time
Sometimes changes that fix a bug introduce other bugs, breaking code that previously worked. In this case the code is said to have “regressed” (gotten worse). Regression testing reruns all tests to check if everything still works (after a change to any part of the code) to ensure that changes made don’t introduce new bugs.