At a fundamental level, programming can be viewed as nothing more than applying functions to arguments. The fact that the Lambda Calculus exists and is shown to be Turing complete means this is true. But we don’t want to program directly in the Lambda Calculus. We like languages with nice-looking, readable syntax. Let’s visit a few examples:
F -- no parens for zero-operand operators F (X, Y, Z) -- three operands for operator F "+" (X, Y) -- abbreviated X + Y ">" (X, Y) -- abbreviated X > Y
f() // parens required for zero-operand calls f(x, y, z) operator+(x, y) // abbreviated x + y operator>(x, y) // abbreviated x > y
(f) (f x y z) (+ x y) (+ a b c d e) ; Not just binary (+ x (* y z))
&f f x, y, z # Hmm, what does this mean? f(x), y, z # Not a call! Just a list of three things f(x, y, z) # Three-operand invocation print 1, 3, sort 4, 2 # Gaaack
f () (* ALL ops MUST have one operand, so () is a value! *) f x y z (* same as ((f x) y) z *) f (x, y, z) (* uncurried *) op+ (x, y) (* abbreviated x + y *)
f // No parens is okay f(x, y, z) // Typical x.+(y) // abbreviated x + y df.format(date) // abbreviated df format date
An operator is basically just a function. But usually, it means a function that has a special status in the language. It is usually but not necessarily named with non-letter (“symbolic”) characters, like “+” or “<” or “=>”. Common alphanumeric operator names are new, delete, and typeof.
Operators have:
Doesa•b¶cmean((a•b)¶c)or does it mean(a•(b¶c))?
Higher precedence operators are applied before lower precedence ones.
Believe it or not, there are so many variations on this simple idea:
I wasn’t kidding about Standard ML. Check this out. When you start things up, Multiplication has precedence level 7 and addition has precedence level 6. We can change them:
5 * 3 + 2; 17 infix 7 +; infix 6 *; 5 * 3 + 2; 25
Doesa•b•cmean((a•b)•c)or does it mean(a•(b•c))or are we not even allowed to write such a thing?
We speak of left associativity, right associativity, and nonassociativity. It is possible, but probably undesirable, for associativity to be unspecified in a language definition 😬.
Variations:
1-5-10 in Python, Smalltalk, and APL.
1-5*10 in Python, Smalltalk, and APL.
1*5-10 in Python, Smalltalk, and APL.
2**3**2 in Python.
-10<-5<-1 in JavaScript, Ruby, Ada, and Python, and explain in detail each of the four completely different behaviors.
The arity of an operator is the allowed number of operands. It can be fixed or variable. A variable arity operator is said to be variadic.
There are a bunch of these: prefix, infix, postfix, overfix, underfix, outfix, and more. Examples in class.
Many language definitions will feature operator tables such as the following:
| Operator | Precedence | Associativity | Arity | Fixity |
|---|---|---|---|---|
** | Highest | R | 2 | Infix |
* / % | L | 2 | Infix | |
Unary -Unary + | R | 1 | Prefix | |
+ - | L | 2 | Infix | |
< <= == != >= > | NO! | 2 | Infix | |
and or | L | 2 | Infix | |
:= | Lowest | R | 2 | Infix |
The content of these tables vary surprisingly among different languages. It’s nice to have such tables so you can see at a glance how the language arranges its operators.
Some languages take pride in having a very small number of precedence levels. Here is the one for Standard ML. It does not mention unary operators because they are just regular functions. And all ”unary” functions bind tighter than (have higher precedence than) binary operators.
![]() | Precedence | Associativity | Arity | Fixity |
|---|---|---|---|---|
* / div mod | 7 | L | 2 | Infix |
+ - ^ | 6 | L | 2 | Infix |
@ :: | 5 | R | 2 | Infix |
< <= = <> >= > | 4 | L | 2 | Infix |
:= o | 3 | L | 2 | Infix |
before | 0 | L | 2 | Infix |
And for Go:
![]() | Precedence | Associativity | Arity | Fixity |
|---|---|---|---|---|
+ - ! ^ * & <- | Highest | 1 | Prefix | |
* / % << >> & &^ | 5 | L | 2 | Infix |
+ - | ^ | 4 | L | 2 | Infix |
< <= == != >= > | 3 | L | 2 | Infix |
&& | 2 | L | 2 | Infix |
|| | 1 | L | 2 | Infix |
Oh, and there is always this old bit of advice: When it doubt, just use lots of parentheses.
When there are subexpressions in a complex expression, must certain subexpressions be evaluated before others? Or can they be evaluated in an arbitrary order? Or even in parallel? Would it even matter?
Java forces a left-to-right ordering: a-f(b)-c*d means do the following, one after another:
| time ① | t0 ← f(b)
|
| time ② | t1 ← a - t0
|
| time ③ | t2 ← c * d
|
| time ④ | t3 ← t1 - t2
|
Most languages allow the evaluation order to be undefined so that the compiler can choose the best order it can. This is especially important for parallel architectures (multiprocessor or multicore).
For example, a–f(b)–c*d can be parallelized as
| time ① | t0 ← f(b) |
Also, a:=b[i]; c:=a*2+d*3 can be done like this:
| time ① | t0 ← b + i | t1 ← d * 3
|
| time ② | a ← *t0 | |
| time ③ | t2 ← a * 2 | |
| time ④ | c ← t1 + t2 |
Undefined ordering can lead to ambiguities or errors:
f(a,g(b)) could yield different results depending on which order the arguments were evaluated.
a:=b; c:=d in parallel, but if a and d are aliases of each other we can’t.
(a+b)-c might behave very differently from (a-c)+b, so a compiler shouldn’t rearrange operands unless it understands these semantics.
$ swift
1> let a: Int8 = 100
let a: Int8 = 100
2> let b: Int8 = 50
b: Int8 = 50
3> let c: Int8 = 40
c: Int8 = 40
4> (a - c) + b
$R0: Int8 = 110
5> (a + b) - c
Execution interrupted. Enter code to recover and continue.
Enter LLDB commands to investigate (type :help for assistance.)
Sometimes the order goes like this: evaluate the left-most operand, then, only if necessary, evaluate the second. You may not even need to evaluate the second at all.
| Expression | Meaning |
|---|---|
e1 andalso e2
| If e1 if falsy, so is the whole and-expression, so you’re done. Otherwise, the result is whatever e2 is. |
e1 orelse e2
| If e1 if truthy, so is the whole or-expression, so you’re done. Otherwise, the result is whatever e2 is. |
Short-circuiting appears frequently in many programming idioms
if (p != null && p.key == value) { ... }
function f(x, y) { // You *can* do this, but JS has default arguments so use those! y ??= 1; ... }
open(F, $file) or die "Can’t open $file: $!";
exists f && remove f
Sometimes the order goes like this: evaluate a designated operand, known as the test, or condition, first. Then depending on the result, evaluate exactly one of the remaining operands, leaving the result unevaluated.
This expression is written in a lot of different ways:
| Form | Language(s) |
|---|---|
| lat >= 0 ? "North" : "South" | Java, JavaScript, Swift, C, C++, C# |
| if lat >= 0 then "North" else "South" | Standard ML, OCaml, Haskell |
| (if lat >= 0 then "North" else "South") | Ada |
| if lat >= 0 then {"North"} else {"South"} | Rust |
| "North" if lat >= 0 else "South" | Python |
| if (lat >= 0) "North" else "South" | Kotlin |
| (if (>= lat 0) "North" "South") | Lisp, Clojure |
| 0 >= "North" "South" ? | Factor |
The short-circuit operators we saw above can be written with conditional operators. Note:
x && y can be written as x ? y : xx || y can be written as x ? x : yThis big idea here is that this conditional expression is not like a typical function call in C-like languages. If it were, it would look something like:
if (test, consequent, alternative)
and in typical languages, all three operands would be evaluated. We want the test to be evaluated, and then only one of the remaining operands to be evaluated, not both.
We’ll see more conditional evaluation and execution in our notes on control flow.
This is a pretty neat segue into our next topic.
If expressions are only evaluated as-needed, or on demand, or only-if-needed, evaluation is said to be lazy. Otherwise it’s eager.
def first(x, y):
return x
first(f(), g())
Under eager evaluation, both f and g are called, and the results of each are passed to first. Under lazy evaluation, only f gets called. In our example, suppose f() evaluates to 3 and g() to 5. Then:
| Eager | Lazy |
|---|---|
first(f(), g())
= first(3, f())
= first(3, 5)
= 3
|
first(f(), g())
= f()
= 3
|
In the cases where g() crashes or has other side effects, the difference between the two strategies can be a big deal.
Note that evaluation order really only matters when side effects can occur (which is why immutability is preferred!). Side effects are what occurs when a storage location is updated or when files or a database are read from or written to.
Storage locations are denoted by lvalues. They are called lvalues because they can appear on the Left side of an assignment. Examples in C:
x
x[4]
y[5].p()->q
*(e1 + e2)
*y[6]
An example of an rvalue is 500 (an integer literal).
Sometimes pretty complicated looking expressions can evaluate to lvalues! Examples:
// C++, but not C (x *= 10) += 7
($x < 5 ? $y : $z) = 10;
$x = "dog"; ${$x} = 2; $x = <STDIN> chomp $x; ${$x} = 2;
val x = ref 0; x := 3; x := !x + 1;
Sometimes, lvalues can be made read-only. Examples:
const in C, let in Swift, val in Kotlin, or final in Java.
Sometimes, immutability is the default, and you have to add keywords or symbols to make an lvalue mutable.
A macro is code that gets expanded into new code which then gets compiled and run. In the simplest case, a macro gets expanded into source code, as in this example in C:
#define area(r) (M_PI*(r)*(r)) double f(x) { return 3 / area(x+10); }
but before the program is compiled, the C preprocessor expands the macro, producing:
double f(x) { return 3 / (M_PI*(x+10)*(x+10)); }
#define area(r) M_PI*r*r;Show the expansion of
3/area(x+10).
More examples of C macros:
#define MAX(x, y) ((x) > (y) ? (x) : (y)) #define mientras while #define forever while(1)
C macros operate in source code. The macros of Lisp, Clojure, and Julia are much more sophisticated: these operate on abstract syntax trees. We’ll cover these when we get to metaprogramming.
Several languages allow you to write code that is evaluated at compile time. Zig even uses the keyword comptime. Others have different syntactic forms.
For no good, reason, here’s a bunch of operators, together with their renderings from a few programming languages. Please don’t assume this list is ordered in any particular way or that it is anywhere near complete.
| Operator | Sample Renderings |
|---|---|
| Arithmetic identity | + |
| Arithmetic negation | - ~ |
| Logical complement | ! not |
| Bitwise complement | ~ not |
| Address of | & ref |
| Dereference | * ^ (postfix) ! |
| Pre-increment | ++ |
| Post-increment | ++ (postfix) |
| Pre-decrement | -- |
| Post-decrement | -- (postfix) |
| Length | length len # |
| Try | ? (postfix) |
| Force unwrap | ! (postfix) |
| Channel read | <- |
| Type | type typeof |
| Operator | Sample Renderings |
|---|---|
| Arithmetic addition | + |
| Arithmetic subtraction | - |
| Arithmetic multiplication | * × |
| Arithmetic division | / ÷ |
| Floor division | // div |
| Arithmetic exponentiation | ** ^ |
| Arithmetic modulus | % mod |
| Arithmetic remainder | % rem |
| Quotient and modulus | divmod /mod |
| Quotient and remainder | quotrem /rem |
| Logical left shift | << lsl |
| Logical right shift | >> lsr |
| Arithmetic left shift | <<< asl |
| Arithmetic right shift | >>> asr |
| Bitwise conjunction | & bitand band |
| Bitwise disjunction | | bitor bor |
| Bitwise xor | ^ bitxor bxor |
| Bitwise negation | ~ not |
| Logical conjunction | && and andalso andthen |
| Logical disjunction | || or orelse |
| Logical negation | ! not |
| Equality | = == === eq |
| Object identity | == === is |
| Not equals | != !== <> /= ~= ne |
| Less than | < lt |
| Less than or equal | <= le |
| Greater than | > gt |
| Greater than or equal | >= ge |
| Comparison | <=> cmp |
| String concatenation | + .. . |
| Forward pipe $x\triangleright f = f(x)$ | |> |
| Backward pipe $f\triangleleft x = f(x)$ | <| $ |
| Forward composition $f\cdot g = \lambda x.g\ (f\ x)$ | >> >>> |
| Backward composition $f\cdot g = \lambda x.f\ (g\ x)$ | << o . |
| Assignment | = := ← |
| Prefix to a list | :: cons |
| Concatenation | @ append ++ |
| Index | !! nth get _[_] |
| Member access | . |
| Optional member access | ?. |
| Dereference then access | -> |
| Exclusive range construction | .. ..< upto |
| Inclusive range construction | ... ..= through |
| Membership | in contains includes ∈ |
| Sequence | , ; then before |
| Regex match | =~ |
| Nullish coalesce | ?? |
| Monadic bind | >>= bind |
| Channel write | ! <- |
| Type conversion | as (_)_ |
| Operator | Sample Renderings |
|---|---|
| Conditional | _?_:_ if_then_else_ _if_else_ |
Sure, it can be confusing that one symbol and mean one thing in one language and something completely different in another. This is a good reason to learn the concepts and the vocabulary. Don’t think in terms of the symbols. Think in terms of the concepts.
Here are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.
-2**2 in JavaScript and Python. Explain why the evaluation produced the value it did in each language. a+(b-c) and (a+b)-c produce different results? (Assume $a$, $b$, and $c$ are simple variables holding integer values.)
var x = 5
function f() { return x = x * 3 }
function g() { return x = x * 5 }
function h(a, b) { return a + x }
print(h(f(), g()))
We’ve covered: