Programs are built from structural components called entities. Perhaps the most common entities are literals, variables, expressions, statements, and declarations. Also common are functions, generators, classes, and modules.
Other kinds of entities are labels, blocks, fields, methods, constructors, destructors, initializers, operators, subscripts, coroutines, continuations, events, threads, tasks, processes, packages, and macros. There are no doubt many more.
Let’s get motivated! Try to identify all the entities in the following programs, taken from various languages. It’s okay if you don’t know all the official terms for them right now. You don’t even have to know the languages very well. You might even think something is an entity when it is not—that’s okay. Just try the exercise for now and see what you come up with:
players = {} for line in io.lines() do team, name, games, points = line:match("([^,]+),([^,]+),(%d+),(%d+)") if tonumber(games) >= 15 then table.insert(players, {name=name, team=team, ppg=points/games}) end end table.sort(players, function (p1, p2) return p1.ppg > p2.ppg end) for index, player in ipairs(players) do if index > 10 then break end format = "%-22s%-4s%8.2f" print(string.format(format, player.name, player.team, player.ppg)) end
import readline from "readline/promises" const reader = readline.createInterface(process.stdin) const players = [] for await (const line of reader) { let [team, name, games, points] = line.split(",") if (games >= 15) { players.push({ name, team, ppg: points / games }) } } const topTen = players.sort((p1, p2) => p2.ppg - p1.ppg).slice(0, 10) for (let { name, team, ppg } of topTen) { console.log( `${name.padEnd(22)}${team.padEnd(4)}${ppg.toFixed(2).padStart(8)}` ) }
import sys from heapq import nlargest from operator import itemgetter players = [] for line in sys.stdin: team, name, games, points = line.strip().split(',') if int(games) >= 15: players.append((name, team, int(points)/int(games))) for name, team, ppg in nlargest(10, players, key=itemgetter(2)): print(f'{name:22}{team:4}{ppg:8.2f}')
using Printf struct Player team::String name::String games::Int ppg::Float64 end function Player(line::Vector{SubString{String}}) points = parse(Float64, line[4]) games = parse(Int, line[3]) return Player(line[1], line[2], games, points / games) end report(p::Player) = @sprintf("%-22s%-4s%8.2f", p.name, p.team, p.ppg) players = [Player(split(line, ",")) for line in eachline(stdin)] top_players = sort(filter(p -> p.games >= 15, players), by = p -> p.ppg, rev = true)[1:10] println.(report.(top_players))
Let’s do it.
Now that we’ve identified some entities, we can now look at the various kinds of entities (with lots of examples, of course). There are dozens of kinds of entities beyond what we’ve seen so far: labels, blocks, modules, routines, functions, operators, classes, fields, methods, modules, macros, and more. The page you are reading now is introductory, so we won’t be covering too much at this time.
A literal is an entity that directly describes a value. You’ve seen dozens of literals already on this page. We couldn’t really help it. Trying to describe values without giving some sort of representation is basically impossible. As a refresher, here are some examples:
95, 0x5F, 0b1011111, 0.0095E4true, false'*'"Hello, how are you?":sent, :inprogress(0, true, 98.6)["ready", "set", "go"](latitude: 29.9792, longitude: 31.1344){:CA => "Sacramento", :HI => "Honolulu, :AK => "Juneau"}x => x / 2null, None, nilundefinedx => x / 2 is new. What is it? If you have a sense that this is very different kind of literal, you’re right.
Don’t worry that the notation is inconsistent. Programming languages differ a great deal in the characters they use for their literals. Relax. Get used to it. Learn all the things. Make it your superpower to be comfortable with the variations and stylistic differences. If you don’t like what you see, create your own programming language.
A variable is a container for a value. It “holds” or “stores” a value.
This is a variable:
This is a variable with a value:
Variables generally have names. Here is a variable with a name:
Here is a variable with a name and a value:
The variable is the box. The variable is NOT the name. The variable is NOT the value.
You can still say “x is 5” but what you really mean is “The variable whose name is x contains the value 5.”
Always remember this.
Always remember this.
Ffs, always remember this!
If a variable is allowed to hold different values at different times, the variable is said to be mutable. If a variable may only hold the first value assigned to it for its entire lifetime, it is said to be immutable.
Where do variables come from? Usually they are brought into existence by a declaration. Here are some examples of variable declarations in different languages:
| Language | Declarations |
|---|---|
| JavaScript | const x = 5 // immutable let x = 5 // mutable |
| Swift | let x = 5 // immutable var x = 5 // mutable |
| Kotlin | val x = 5 // immutable var x = 5 // mutable |
| Go | const x = 5 // immutable var x = 5 // mutable |
| Java | final var x = 5 // immutable var x = 5 // mutable |
| Rust | let x = 5 // immutable let mut x = 5 // mutable |
In some languages, variables don’t have to be declared at all, they just pop into existence when they are first used, though often with some restrictions. Lua even allows this for some kinds of variables but not others.
In many languages, you can declare more than one variable at a time using pattern matching. Here are examples in JavaScript:
let [x, y] = [5, 7] // Array pattern on left, array expression on right // x is 5 and y is 7 let {from: x, to: y} = {from: 5, to: 7} // Object pattern on left, object expression on right // x is 5 and y is 7 let {name: name, age: age} = {name: "Alice", age: 33} // name is "Alice" and age is 33 let {name, age} = {name: "Alice", age: 33} // name is "Alice" and age is 33 // note the awesome shorthand property syntax let [a, b, ...rest] = [1, 2, 3, 4, 5] // a is 1, b is 2, and rest is [3, 4, 5] let {first, second, ...others} = {first: 1, second: 2, third: 3, fourth: 4} // We used the shorthand property syntax again bc it is great // first is 1, second is 2, and others is {third: 3, fourth: 4}
Technically, a declaration creates a binding of a name to an entity. (Notice we said “to an entity” and not “to a variable” because other entities besides variables can be named.)
Bindings have spatial scope and temporal extent. Scope (almost always) refers to the textual (spatial) region of the code in which the binding can be used. Extent is the (temporal) duration from the time an entity is bound to a name to the time the binding is deactivated, either by reassignment or reaching the end of a block. The entity itself has a (temporal) lifetime, the during from which the entity comes into existence to the time it is destroyed. Many programming errors and security vulnerabilities occur when the extents and lifetimes are not properly managed.
Later we will study much, much more about bindings. There is so much to learn.
Time to talk about computation. An expression is made up of literals and variables connected with zero or more operators, that when evaluated, produces a new value. Here are some examples in Python, assuming x stores the value 5:
| The expression | 2 + x | evaluates to | 7 |
| The expression | 2 - x | evaluates to | -5 |
| The expression | 2 * x | evaluates to | 10 |
| The expression | 2 / x | evaluates to | 0.4 |
| The expression | 2 // x | evaluates to | 0 |
| The expression | 2 % x | evaluates to | 2 |
| The expression | 2 ** x | evaluates to | 32 |
| The expression | 2 < x | evaluates to | true |
| The expression | 2 > x | evaluates to | false |
| The expression | 2 == x | evaluates to | false |
In Programming Language Theory, the “evaluates to” relation is frequently written ⇓. Here is a more complex expression evaluation in JavaScript. Assuming s holds the value "car", y holds the value 100, and found holds the value true, then:
7 * s.indexOf('r') + Math.sqrt(y) / 2 <= 0 || !found ⇓ true
Here’s an expression for one root of the famous quadratic formula expressed in several languages:
| Language | Expression |
|---|---|
| Lua | (-b + math.sqrt(b^2 - 4*a*c)) / (2*a) |
| JavaScript | (-b + Math.sqrt(b ** 2 - 4 * a * c)) / (2 * a) |
| Python | (-b + (b ** 2 - 4 * a * c) ** 0.5) / (2 * a) |
| Java | (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a) |
| Kotlin | (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a) |
| Rust | (-b + (b * b - 4.0f64 * a * c).sqrt()) / (2.0 * a) |
| Clojure | (/ (+ (- b) (Math/sqrt (- (* b b) (* 4 a c)))) (* 2 a)) |
| Swift | (-b + (b * b - 4 * a * c).squareRoot()) / (2 * a) |
| OCaml | (~-.b +. sqrt (b *. b -. 4. *. a *. c)) /. (2. *. a) |
Some expressions do more than just produce values. Along the way, they may generate effects that change the external computational environment, or context, in which the program runs. A great example is Python’s := operator, which makes an assignment in the middle of an expression! Compare:
while True:
command = input('> ')
if current == 'Q':
break
execute(command)
while (command := input('> ')) != 'Q':
execute(command)
There is so much to learn about expressions! Operators have precedence, associativity, arity, and fixity. There are various ways specify an evaluation order among expressions. Some operators can actually short-circuit evaluations. And there is more to side-effecting expressions than the simply Python operator we saw above.
For a deeper study of expressions and expression evaluation, see these notes.
Statements (also known as commands) are entities executed purely for their effects—they do not produce values. You may encounter:
if, unless, switch, case, when, or match.loop, while, repeat, until, or for.break, continue, retry, redo, return, yield, resume, throw, or raise.In Python, we use = for assignment statements and := for assignments in expressions. In Lua and Go, assignments are (by design) statements only. In the C family of languages (including C, C++, C#, Java, Kotlin, and JavaScript), assignments are always expressions, but you can use them in an expression statement.
A function is a computation from (zero or more) inputs to (zero or more) outputs. The big idea is a function is a parameterized expression or block of statements.
Here is the way we express the cube function in several languages:
| Language | Function Expression |
|---|---|
| JavaScript | x => x ** 3 |
| Python | lambda x: x ** 3 |
| Ruby | ->(x) { x ** 3 } |
| OCaml | fun x -> x * x * x |
| Haskell | \x -> x ^ 3 |
| Java | (x) -> x * x * x |
| Kotlin | (x: Int) -> x * x * x |
| Scala | (x: Int) => x * x * x |
| Swift | {$0 * $0 * $0} |
| Go | func(x int) int { return x * x * x } |
| Rust | |x: i32| x * x * x |
| Lua | function(x) return x^3 end |
| Erlang | fun(X) -> X * X * X |
| Elixir | &(&1 * &1 * &1) |
| Gleam | fn(x) { x * x * x } |
| K | {x^3} |
| Factor | [ dup * * ] |
Note that functions are a kind of value, so we can assign them to variables (either in a variable declaration or an assignment statement):
| Language | Assignment |
|---|---|
| Lua | cube = function(x) return x^3 end |
| JavaScript | cube = x => x ** 3 |
| Python | cube = lambda x: x ** 3 |
We can also pass functions to functions:
| Language | Higher-Order Function |
|---|---|
| Lua | twice = function(f, x) return f(f(x)) end |
| JavaScript | twice = (f, x) => f(f(x)) |
| Python | twice = lambda f, x: f(f(x)) |
And we can return functions from functions:
| Language | Function Returning a Function |
|---|---|
| Lua | add = function(n) return function(x) return x + n end end |
| JavaScript | add = n => x => x + n |
| Python | add = lambda n: lambda x: x + n |
So here add(5)(3) produces 8. That’s because add(5) is a function that adds 5 to its argument. So applying it to 3 yields 8. It’s not weird or special or anything like that. It’s just natural.
Functions don’t always have to be so simple as to be written with a body that is just an expression. Usually you can write a function declaration which creates a function with a complex body and binds a name to it, using a simpler notation than that used when assigning function values. Often, but certainly not always, a return statement, if present, produces the result of the function.
function change(amount) if math.type(amount) ~= "integer" then error("Amount must be an integer") end if amount < 0 then error("Amount cannot be negative") end local counts, remaining = {}, amount for _, denomination in ipairs({25, 10, 5, 1}) do counts[denomination] = remaining // denomination remaining = remaining % denomination end return counts end
function change(amount) { if (!Number.isInteger(amount)) { throw new TypeError('Amount must be an integer') } if (amount < 0) { throw new RangeError('Amount cannot be negative') } let [counts, remaining] = [{}, amount] for (const denomination of [25, 10, 5, 1]) { counts[denomination] = Math.floor(remaining / denomination) remaining %= denomination } return counts }
def change(amount): if not isinstance(amount, int): raise TypeError('Amount must be an integer') if amount < 0: raise ValueError('Amount cannot be negative') counts, remaining = {}, amount for denomination in (25, 10, 5, 1): counts[denomination], remaining = divmod(remaining, denomination) return counts
A procedure is a function with no outputs. A consumer is a function with no outputs but at least one input. A supplier is a function with no inputs but at least one output. A predicate is a function that produces a boolean result.
Procedures and FunctionsWhile many programming languages use the term function for the general executable code block and consider a “procedure” to be a non-returning function, some languages go the other way and use procedure as the general term and speak of “value-returning procedures” when they have functions is mind. Some languages, notably Ada, wisely imho use both keywords
procedureandfunction! Fortran uses two keywords also:functionfor the value-returning entity and “subroutine” for the non-value-returning case.The vocabulary isn’t as consistent as we may like.
Let’s see a use case for predicates. The popular function filter accepts a sequence and a predicate, and returns a new sequence containing only the elements for which the predicate is true. Here it is in action in a few languages:
list(filter(lambda x: x % 2 == 0, range(10)))
Array.from({length: 10}, (_, i) => i).filter(x => x % 2 === 0)
IntStream.range(0, 10).filter(x -> x % 2 == 0).toArray()
(0 until 10).filter { it % 2 == 0 }
Array(0..<10).filter { $0 % 2 == 0 }
(0..10).filter(|&x| x % 2 == 0).collect()
filter(0, 10, func(x int) bool { return x % 2 == 0 })
List.filter (fun x -> x mod 2 = 0) (List.init 10 (fun x -> x))
filter even [0..9]
lists:filter(fun(X) -> X rem 2 =:= 0 end, lists:seq(0, 9))
List.filter(fn(x) {x rem 2 == 0}, List.range(0, 10))
When calling or invoking functions, arguments are assigned to parameters. Read that again: argument passing is just assignment. Sometimes, languages will have a special syntax just for passing arguments in a readable fashion. Here’s a Python function definition:
def set_line_attributes(thickness, style, color): # ...
The function can be called in many ways, including
set_line_attributes(3, "dashed", "blue") set_line_attributes(3, color="blue", style="dashed") set_line_attributes(thickness=3, style="dashed", color="blue") set_line_attributes(color="blue", thickness=3, style="dashed") set_line_attributes(3, style="dashed", color="blue") # ...
If we simply lay out the arguments in order, they match up positionally, and are called positional arguments. If we mention the parameter name, they are called keyword arguments or kwargs. Generally, positional arguments have to come first. Kwargs can go in any order.
If you think about it, the designer of this function should force callers to use kwargs, since how can you possibly remember the parameter order? In Python, all parameters after a * must be kwargs. Here’s a better version of the function:
def set_line_attributes(*, thickness, style, color): # ...
Sometimes using a kwarg is silly, so we want to force callers to use positional args only:
def square_root(x, /): # ...
You can have both a slash and star:
def f(a, b, /, c, d, *, e, f): # ...
Here a and b must be passed positionally, e and f must be passed via kwargs, and you may pass c and d either way.
In languages that support pattern matching in declarations and assignment, you don’t really need a special syntax. Because argument passing is just assignment, you’ll likely get the pattern matching for free when passing arguments! This is very common in JavaScript.
function setLineAttributes({thickness, style, color}) { // ... } setLineAttributes({style: "dashed", thickness: 3, color: "blue"})
Here’s something cute:
function push({onTheStack: s, theValue: x}) { s.push(x) } push({onTheStack: breadcrumbs, theValue: 5}) push({theValue: 8, onTheStack: breadcrumbs})
Read those calls. See what we did there?
An advantage of this approach over Python’s kwargs is that you can use a different name for the parameter inside the function than the name used in the call. Slay.
More on argument passingThere are a lot of different variations on argument-passing, so many that we have to cover them later in more extensive set of notes, in which we’ll see pattern matching in argument passing, default parameters, variadic functions, argument labels, lazy argument evaluation, and semantic variations in the way arguments are matched to parameters. The number of things to look at is 🤯.
Typically, when calling a function, the caller is blocked (that is, it waits) until the function returns. But what if the caller doesn’t block? That’d be an asynchronous call. One way to do this is to use promises: when the caller makes the call, the call returns immediately with a promise object, while the callee itself starts running on a separate line of execution, concurrently with the caller. The promise object itself can be inspected to see the status of the spawned line of execution, or it can be given a function to run when the spawned line is completed. Or even two functions: one to call on a successful completion and one to call if an error occurred in the spawned line.
For more on promises and asynchronous programming in general, see these notes. (They are JavaScript-specific, but they get the main points across.)
A generator is a function-like object that produces the successive values of a sequence, one value per call. That is, you can get the values, one at a time, on demand, when and only when you need them. This means you don’t have to get them all at once, which is nice because:
Generators are interesting because they are stateful. Many languages have some interesting syntax to make them. Usually, you call a function to produce the generator, then invoke a next operation to get the next value. But the cool thing is that you can often use the generator-producing function in a for-loop. Python and JavaScript work in this fashion.
Here are a couple generators in Python. The first generates the sequence 1, 2, 3 then stops. The second generates Fibonacci numbers—forever:
# A finite generator def one_two_three(): yield 1 yield 2 yield 3 # Build the actual generator and get its values with next() g = one_two_three() print(next(g)) # 1 print(next(g)) # 2 print(next(g)) # 3 try: # In Python, if the generator is exhausted, it raises StopIteration print(next(g)) except StopIteration: print("No more values") # The generator-producing function can be used directly in a for loop for value in one_two_three(): print(value) # If the generator is finite, you can do this too print(list(one_two_three())) # [1, 2, 3] # Now, an infinite generator def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b # Build the actual generator and get its values with next() f = fibonacci() print(next(f)) # 0 print(next(f)) # 1 print(next(f)) # 1 print(next(f)) # 2 print(next(f)) # 3 # If an infinite generator is used in a for loop, you need to break out sometime for f in fibonacci(): if f > 1000000000: break print(f)
JavaScript generators are similar, but must be introduced with function*:
function* oneTwoThree() { yield 1 yield 2 yield 3 } const g = oneTwoThree() console.log(g.next()) // { value: 1, done: false } console.log(g.next()) // { value: 2, done: false } console.log(g.next()) // { value: 3, done: false } console.log(g.next()) // { value: undefined, done: true } console.log([...oneTwoThree()]) // [1, 2, 3] function* fibonacci() { let [a, b] = [0, 1] while (true) { yield a ;[a, b] = [b, a + b] } } const f = fibonacci() console.log(f.next()) // { value: 0, done: false } console.log(f.next()) // { value: 1, done: false } console.log(f.next()) // { value: 1, done: false } console.log(f.next()) // { value: 2, done: false } console.log(f.next()) // { value: 3, done: false } for (const f of fibonacci()) { if (f > 1000000000) { break } console.log(f) }
yield, JavaScript has yield*. Do some research to find out when you might use this construct.
A coroutine is an executable block fo code like a function, but instead of always starting at the beginning and running all the way to the end every time (like a function does), a coroutine can be paused and resumed, allowing it to yield control back to the caller and later continue execution from where it left off.
Coroutines can be used for many things, including implementing generators. Here’s how it’s done in Lua:
function one_two_three() return coroutine.create(function () coroutine.yield(1) coroutine.yield(2) coroutine.yield(3) end) end function fibonacci() return coroutine.create(function () local a, b = 0, 1 while true do coroutine.yield(a) a, b = b, a + b end end) end g = one_two_three() print(coroutine.resume(g)) -- true, 1 print(coroutine.resume(g)) -- true, 2 print(coroutine.resume(g)) -- true, 3 print(coroutine.resume(g)) -- true, nil print(coroutine.resume(g)) -- false, "cannot resume dead coroutine" g = fibonacci() for i = 1, 20 do status, value = coroutine.resume(g) print(value) end
Fun fact: a function is just a coroutine that does not have a yield. Coroutines are the more general concept.
For more on coroutines, see these notes.
What’s really interesting about many records is not just the values of their components, but how they behave. We like to bundle up the data and the behavior into objects. A class is an entity that defines the structure and behavior of objects, as well as how to create them. You will sometimes hear that a class defined as a factory for creating objects with the same structure and behavior. Here is an example of a class that creates two-dimensional mutable vectors in Python, JavaScript, and Lua:
class Vector:
def __init__(self, i, j):
self.i = i
self.j = j
def __add__(self, v):
return Vector(self.i + v.i, self.j + v.j)
def __sub__(self, v):
return Vector(self.i - v.i, self.j - v.j)
def dot(self, v):
return self.i * v.i + self.j * v.j
def __eq__(self, v):
return self.i == v.i and self.j == v.j
@property
def magnitude(self):
return (self.i ** 2 + self.j ** 2) ** 0.5
@property
def normalized(self):
m = self.magnitude
return Vector(self.i / m, self.j / m)
def __str__(self):
return f'<{self.i}, {self.j}>'
u = Vector(3, 4)
v = Vector(1, 2)
print(u)
print(v)
print(u + v)
print(u - v)
print(u.dot(v))
print(u.magnitude)
print(u.normalized)
print(u == Vector(3, 4))
print(u == Vector(4, 3))
class Vector {
constructor(i, j) {
Object.assign(this, { i, j })
}
plus(v) {
return new Vector(this.i + v.i, this.j + v.j)
}
minus(v) {
return new Vector(this.i - v.i, this.j - v.j)
}
dot(v) {
return this.i * v.i + this.j * v.j
}
equals(v) {
return this.i === v.i && this.j === v.j
}
get magnitude() {
return Math.sqrt(this.i * this.i + this.j * this.j)
}
get normalized() {
const m = this.magnitude
return new Vector(this.i / m, this.j / m)
}
toString() {
return `<${this.i}, ${this.j}>`
}
}
const u = new Vector(3, 4)
const v = new Vector(1, 2)
console.log(u.toString())
console.log(v.toString())
console.log(u.plus(v).toString())
console.log(u.minus(v).toString())
console.log(u.dot(v))
console.log(u.magnitude)
console.log(u.normalized.toString())
console.log(u.equals(new Vector(3, 4)))
console.log(u.equals(new Vector(4, 3)))
Vector = (function (class)
local meta = {
__add = function(self, v)
return class.new(self.i + v.i, self.j + v.j)
end,
__sub = function(self, v)
return class.new(self.i - v.i, self.j - v.j)
end,
__mul = function(self, v)
return self.i * v.i + self.j * v.j
end,
__eq = function(self, v)
return self.i == v.i and self.j == v.j
end,
__tostring = function(self)
return '<' .. self.i .. ', ' .. self.j .. '>'
end,
__index = {
magnitude = function(self)
return math.sqrt(self.i * self.i + self.j * self.j)
end,
normalized = function(self)
local m = self:magnitude()
return class.new(self.i / m, self.j / m)
end
},
}
class.new = function (i, j)
return setmetatable({i = i, j = j}, meta)
end
return class
end)({})
u = Vector.new(3, 4)
v = Vector.new(1, 2)
print(u)
print(v)
print(u + v)
print(u - v)
print(u * v)
print(u:magnitude())
print(u:normalized())
print(u == Vector.new(3, 4))
print(u == Vector.new(4, 3))
The idea behind classes is the ability to create many objects with the same structure and behavior, but with different internal state. The state is captured by an object’s properties and its behavior through methods. You can think of methods as functions that are ”tied to” an object (that read and manipulate the object’s state). There is such a wide variety in the way classes are implemented in different languages that we’ll be covert them in separate notes on classes.
It’s generally better practice to make immutable objects, as these are more secure. Let’s see how this is done, first in Python. We’ll need to use the @dataclass decorator and freeze the object. Begin the code like so:
from dataclasses import dataclass @dataclass(frozen=True) class Vector: x: float y: float # Rest of the class goes here
And in JavaScript, you’ll need to use the Object.freeze method:
class Vector { constructor(x, y) { this.x = x this.y = y Object.freeze(this) } // Rest of the class goes here }
For Lua, we don’t really bother. There are tricks, but they are inefficient, or they are subvertible with the rawset function. A derivative language, Luau, does have a table.freeze() function.
There’s another layer of security that is often employed when creating classes, namely information hiding. In many languages, we can make the properties of the objects created by classes completely invisible to code outside the class. This is generally done by marking various properties private. Privacy is a specific instance of the much more general topic of access control, which we’ll discuss elsewhere, in our expanded and detailed notes on classes.
Classes are not TypesThere is a huge difference between a class and a type. The class of an object is available to you at runtime, via an operation such as
.getClass()or.constructor, while types are part of the static (pre-execution) context of a language. Details will come later when we cover types in more detail.
Think of a module as a container for a bunch of related entities. That’s good enough for now. Later we’ll study how modules in different languages are used not only for encapsulation but for information hiding, too. We’ll learn about importing and exporting. Modules are necessary for humans to understand massive software systems.
You can find some very simple notes on modules here, but you’ll learn much more by studying and using the module systems of many different languages. There are a lot of different approaches out there.
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.
Object.freeze to freeze the object upon construction.We’ve covered: