Entities

To effectively study programming languages, we need to understand the components from which programs are constructed. The units of programs are called entities.

Program Construction

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))
CLASSWORK
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.

Literals

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:

Exercise: Wait, that x => 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.

Variables

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:

5

Variables generally have names. Here is a variable with a name:

x

Here is a variable with a name and a value:

5 x

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.

Exercise: What is the difference between a literal and a variable?

Declarations

Where do variables come from? Usually they are brought into existence by a declaration. Here are some examples of variable declarations in different languages:

LanguageDeclarations
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.

Exercise: Commit to memory the fact that bindings have extents and entities have lifetimes.
Exercise: Imagine cases in which the extent of a binding of a variable is longer than the lifetime of the entity it refers to.

Expressions

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 expression2 + xevaluates to7
The expression2 - xevaluates to-5
The expression2 * xevaluates to10
The expression2 / xevaluates to0.4
The expression2 // xevaluates to0
The expression2 % xevaluates to2
The expression2 ** xevaluates to32
The expression2 < xevaluates totrue
The expression2 > xevaluates tofalse
The expression2 == xevaluates tofalse

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 || !foundtrue

Here’s an expression for one root of the famous quadratic formula expressed in several languages:

LanguageExpression
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)
Exercise: What is going on with that Clojure syntax?

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

Statements (also known as commands) are entities executed purely for their effects—they do not produce values. You may encounter:

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.

Exercise: Tell a friend the difference between an expression and a statement.

Functions

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:

LanguageFunction 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):

LanguageAssignment
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:

LanguageHigher-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:

LanguageFunction 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 Functions

While 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 procedure and function! Fortran uses two keywords also: function for 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.

Exercise: Swift, by default, uses argument labels to allow the names on call arguments to be different than those used for parameters. Research Swift argument labels. Rewrite the above example for stack pushing in Swift.
More on argument passing

There 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.)

Generators

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)
}
Exercise: In addition to yield, JavaScript has yield*. Do some research to find out when you might use this construct.

Coroutines

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.

Classes

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:

vector.py
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))
vector.js
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.lua
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 Types

There 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.

Modules

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.

Recall Practice

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.

  1. What is an entity?
    A language construct from which programs are constructed.
  2. What is a literal?
    An entity that directly describes a value.
  3. What is a variable?
    An entity that is a container for a value.
  4. What do people sometimes confuse variables with?
    The name of the variable, or the value contained within the variable.
  5. A variable whose value can be replaced with another value is said to be ________________.
    mutable.
  6. What kind of entity brings variables into existence?
    A declaration.
  7. What mechanism is used to declare multiple variables in a single construct?
    Pattern matching.
  8. What is an expression?
    An entity made up of variables, literals, and operators that describes a computation and produces a value when evaluated.
  9. What is a statement?
    An entity that describes an action to be carried out, but that does not produce a value.
  10. What is a function?
    An entity that describes a computation from zero or more inputs to zero or more outputs.
  11. What happens when a function is called?
    Arguments are assigned to parameters, and the function’s body is executed.
  12. What are the two kinds of arguments in Python?
    Positional arguments and keyword arguments.
  13. How do you simulate kwargs in JavaScript?
    By passing an object in the call.
  14. How does a generator differ from a typical function?
    A generator produces a sequence of values, one at a time, each time it is called.
  15. What is a class?
    An entity that defines the structure and behavior of a family of objects and how to create them.
  16. How do you make an object immutable in JavaScript?
    By using Object.freeze to freeze the object upon construction.
  17. What is a module?
    A container for a bunch of related entities, usually with import and export controls.

Summary

We’ve covered:

  • What an entity is
  • Literals
  • Variables
  • Declarations
  • Expressions
  • Statements
  • Functions
  • Generators
  • Classes
  • Modules