---
title: 'Constants and Variables'
source: 'https://academia.sh/en/courses/swift/constants-and-variables'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:11+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explain the difference between `let` and `var` through assignment behavior'
    - 'Compare type inference with explicit type annotation'
    - 'Interpret the evaluation moment of deferred initialization and of reassignment'
    - 'Read from sorted output that a global constant in another file is set up on first access'
---

# Constants and Variables

Measures `let` and `var` together with type inference, explicit type annotation and the moment of assignment: is a constant evaluated immediately, and is an assignment evaluated every time?

In the previous lesson you saw that a `let` constant is evaluated **immediately**, on the line where it is defined. This lesson pushes that observation from three directions: does writing a type on a constant change anything, what does `var` do on each assignment, and does the moment shift if the value is assigned after the definition? You know variables and types from the Programming Fundamentals course (M01/K02); here we measure only what is specific to Swift.

## Constants, Variables and Types

In Swift, `let` defines a constant that is bound once, and `var` defines a variable that can change later. You usually do not write the type: the compiler infers it from the right-hand side (type inference). If you like, you write the type after a colon (explicit type annotation). A second assignment to a constant is rejected by the compiler: the snippet below does not compile in Swift 6.4, which is why it has no output.

```swift
let rate = 4
rate = 5
```

The same assignment is allowed for `var`. The difference is not only in the syntax: a `var` line evaluates its right-hand side again on every assignment.

## Three Forms of Assignment

First the `Probe.swift` helper file; it stays the same in every lesson.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

Now three `Probe` objects: one tracks a constant whose type is inferred, one a constant whose definition and assignment are separated, and one a variable that is assigned three times.

```swift
// main.swift — assignment moments of a constant and a variable
let inferredProbe = Probe("inferred-constant")
let deferredProbe = Probe("deferred-constant")
let counterProbe = Probe("variable-assignment")

let rate = inferredProbe.eval(4)
let explicitRate: Double = 4
_ = inferredProbe.use { rate }
_ = inferredProbe.use { rate }
print("\(type(of: rate)) \(type(of: explicitRate))")

let isWeekend = false
let level: Int
if isWeekend {
    level = deferredProbe.eval(1)
} else {
    level = deferredProbe.eval(2)
}
_ = deferredProbe.use { level }
_ = deferredProbe.use { level }

var total = counterProbe.eval(0)
total = counterProbe.eval(total + 5)
total = counterProbe.eval(total + 7)
_ = counterProbe.use { total }
_ = counterProbe.use { total }

let allProbes = [inferredProbe, deferredProbe, counterProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
```

```
Int Double
inferred-constant: 1 evaluations, 2 uses -> immediately
deferred-constant: 1 evaluations, 2 uses -> immediately
variable-assignment: 3 evaluations, 2 uses -> on-every-use
immediately: 2
on-first-use: 0
on-every-use: 1
never: 0
```

The first line shows the types: the `4` written without a type is an `Int`, and the same number declared as `Double` is a `Double`. Type inference did not change the evaluation moment; the inferred constant was evaluated on the line where it is defined, before the first use, so it is `immediately`.

`level` is the interesting case: an `if` sits between the definition (`let level: Int`) and the value, but the assignment still happened before any use. Both branches call `eval`, but only one of them runs; the count is one, and the moment is again `immediately`. The compiler rejects a read before the assignment (the first snippet of the previous lesson), so no read can sit between the definition and the assignment; but if the assignment is made inside a wrapper around a use, the measurement gives `on-first-use` (see the Deferred Initial Value section below).

`var` is different. `total` was assigned three times, and each assignment evaluated its right-hand side again; the rule reads this as `on-every-use`. A variable is not a constant bound once; every assignment is a new evaluation.

**LB8.** The assignments to `counterProbe` happen before the uses, but because there are three evaluations the rule says `on-every-use`; when the count is more than one, "before or after" is not examined.

**LB9.** The `type(of:)` output prints only the names `Int` and `Double`; a type name points at the result of inference, not at the measurement axis.

## Type Inference and Explicit Type Annotation

When you define a constant or a variable, you do not have to write the type. On the line `let attempts = 3`, the compiler infers `Int` from the literal on the right; this is called type inference. If you like, you write the type explicitly after a colon: `let ratio: Double = 3`. This is called explicit type annotation. The only difference between the two is who decides the type: with inference the type comes from the value, with an explicit annotation the form of the value comes from the type.

The difference shows most when you write a literal number. The literal `3` becomes an `Int` under inference, but if you write `Double` on the left, the same literal is read as `3.0`. The block below binds the same literal `3` in two ways, passes each through a `Probe` and reads each twice.

```swift
// Literal.swift — the same literal, two types
let inferredProbe = Probe("inferred-int")
let declaredProbe = Probe("explicit-double")
let attempts = inferredProbe.eval(3)
let ratio: Double = declaredProbe.eval(3)
_ = inferredProbe.use { attempts }
_ = inferredProbe.use { attempts }
_ = declaredProbe.use { ratio }
_ = declaredProbe.use { ratio }
print(attempts, ratio)
print(type(of: attempts), type(of: ratio))
inferredProbe.report()
declaredProbe.report()
```

```
3 3.0
Int Double
inferred-int: 1 evaluations, 2 uses -> immediately
explicit-double: 1 evaluations, 2 uses -> immediately
```

`attempts` was printed as `3` and `ratio` as `3.0`; the `type(of:)` line confirms that the types are `Int` and `Double`. The literal was written the same way, but the type coming from the context turned it into two different values. The two `Probe` objects gave the same moment: both are `immediately`. The type determines only what the value is, not when it is evaluated. The compiler finds the type without running the code; all that remains at run time is a single `eval` call.

So an explicit type annotation does two jobs. The first is to ask for a type different from the one inference would choose. The second is to show the reader the intent behind the value. On the measurement axis, the two are equal.

**LB10.** Inference and annotation were measured with the same literal `3`; how early the type is found in a more complex expression lies outside this lesson.

## Tuple Decomposition and Several Definitions on One Line

Swift lets you define more than one name on a single constant or variable line. You can write `var left = 0, right = 0` separated by a comma; each name is bound to its own expression separately. Another way is to take a tuple apart: `let (low, high) = (1, 9)` defines two constants and distributes the elements of the tuple to them in order. This syntax is known as tuple decomposition.

The question that comes to mind is this: when two names are defined on the same line, are their expressions evaluated at the same moment, or does one come before the other? The block below defines two names from a tuple with two separate `Probe` objects, then defines two `var` names on a single line and assigns them in turn.

```swift
// Decompose.swift — tuple decomposition and several definitions on one line
let lowProbe = Probe("lower-bound")
let highProbe = Probe("upper-bound")
let (low, high) = (lowProbe.eval(1), highProbe.eval(9))
_ = lowProbe.use { low }
_ = lowProbe.use { low + 1 }
_ = highProbe.use { high }
_ = highProbe.use { high - 1 }
var left = 0, right = 0
left = lowProbe.eval(low)
right = highProbe.eval(high)
_ = lowProbe.use { left }
_ = highProbe.use { right }
print(low, high, left, right)
lowProbe.report()
highProbe.report()
```

```
1 9 1 9
lower-bound: 2 evaluations, 3 uses -> on-every-use
upper-bound: 2 evaluations, 3 uses -> on-every-use
```

Both elements of the tuple were evaluated on the definition line, before any read. But that was not the only evaluation: `lowProbe` and `highProbe` each showed one evaluation, and then the assignments to `left` and `right` added a second one each. That is why both measurement lines gave two evaluations, three uses and `on-every-use`. A definition on one line or a tuple decomposition does not merge the expressions into a single step; each element is evaluated as many times as its own expression.

The principle here is this: how many times an expression is evaluated is decided not by how many lines you write it on, but by how many evaluation points like `eval` there are. Tuple decomposition shortens the writing; it does not change the order at run time.

**LB11.** Tuple decomposition was measured with two elements and constant literals; the order in which nested tuples are taken apart lies outside this lesson.

## Shadowing

In an inner scope you can redefine the same name as in an outer scope. This is called shadowing: the name in the inner scope hides the one in the outer scope until the scope ends. The outer variable or constant does not change in any way; only the reads in the inner scope go to the new name. This behavior is the safe way to transform a value temporarily and keep using the same name.

Shadowing is not a new assignment. When `amount` is defined a second time with `let`, the outer `amount` stays as it is; the new definition is a second evaluation, and it lives only in its own scope. The block below shadows the outer constant `amount` inside a `do` scope, prints the inner value, leaves the scope and prints the outer value again.

```swift
// Shadow.swift — shadowing does not change the outer value
let outerProbe = Probe("outer-scope")
let innerProbe = Probe("inner-scope")
let amount = outerProbe.eval(1)
do {
    let amount = innerProbe.eval(amount + 10)
    _ = innerProbe.use { amount }
    _ = innerProbe.use { amount * 2 }
    print(amount)
}
_ = outerProbe.use { amount }
_ = outerProbe.use { amount + 5 }
print(amount)
outerProbe.report()
innerProbe.report()
```

```
11
1
outer-scope: 1 evaluations, 2 uses -> immediately
inner-scope: 1 evaluations, 2 uses -> immediately
```

Inside the inner scope `amount` printed 11: the `amount` on the right side of the inner definition was still the outer constant, because the inner name had not been bound yet. After leaving the scope, `amount` was 1 again; shadowing never changed the outer constant. The two `Probe` objects each showed one evaluation and two uses: both are `immediately`. Shadowing adds a new evaluation, but because it is not a reassignment, it does not raise the count on the outer side.

In practice, shadowing is a tidy way to transform a value without multiplying variable names. In return, the reader has to know which `amount` they are looking at; keeping the scope short reduces that burden.

**LB12.** Shadowing was measured with a `do` scope; the behavior of shadowing a function parameter or a loop variable lies outside this lesson.

## Deferred Initial Value

You do not have to give a `let` constant its value on the line where it is defined. You can write the type and assign the value later, as long as the compiler can see that the constant receives a value exactly once on every path. This rule keeps the "unchangeable" meaning of the `let` declaration: a constant cannot be read until it receives its first value, and it cannot be reassigned after that. In a branching flow, each branch fills the constant once, and the constant is ready at the end of the branch.

This has an effect that can be measured: only the expression of the chosen branch runs. The block below writes a function. In the function, the constant `label` is declared first, then filled with a value from the `Probe` in one branch and with a fixed string in the other branch through an `if`. The function is called first with `true`, then with `false`.

```swift
// Deferred.swift — deferred initial value
func pick(open: Bool, probe: Probe) -> String {
    let label: String
    if open {
        label = probe.eval("open")
    } else {
        label = "closed"
    }
    return label
}
let pickProbe = Probe("deferred-constant")
print(pickProbe.use { pick(open: true, probe: pickProbe) })
print(pickProbe.use { pick(open: false, probe: pickProbe) })
pickProbe.report()
```

```
open
closed
deferred-constant: 1 evaluations, 2 uses -> on-first-use
```

The `true` call gave `open` and the `false` call gave `closed`. The compiler accepted this because a new `label` constant is defined on each call and each one is filled once. The `Probe` showed one evaluation for two uses: `probe.eval` ran only in the `true` branch, and the fixed string of the `false` branch never touched the `Probe`. An expression that is evaluated on first use and does not run again is the mark of the `on-first-use` moment; here the second call never reached that expression.

This behavior shows that deferred initial value is a rule: every path fills the constant exactly once, and a path that does not fill it computes nothing. The value of the constant comes from the expression of whichever branch the flow enters. If you forget the assignment in a branch, the compiler does not let you use the constant after that point.

The rule is also useful when you prepare the return value of a function: the constant is declared, the branches fill it, and a single `return` is left at the end of the function. Using `var` and changing it later would also work, but `let` tells the compiler and the reader that the value is assigned once.

**LB13.** Deferred initial value was measured with two branches and inside a function; the rules of the same structure in top-level code or in properties lie outside this lesson.

## The Bounding Measurement

A constant being evaluated **immediately** does not hold everywhere. A global constant defined in another file is set up on first access. The helper file below puts a `print` in the initialization closure; the closure is inside the `let` definition.

```swift
// Config.swift — a global constant in another file
let appName: String = {
    print("setting up appName")
    return "academia"
}()
```

`main.swift` defines a local constant with the same pattern and shows, in line order, when the two constants are set up.

```swift
// main.swift — setup moment of a global and a local constant
print("start")
let localName: String = {
    print("setting up localName")
    return "academia"
}()
print("second line")
print("read: \(appName)")
print("local: \(localName)")
```

```
start
setting up localName
second line
setting up appName
read: academia
local: academia
```

`localName` was set up in the top level of `main.swift`, on the line where it is written (`immediately`); `appName` was set up after the second line, inside the `print` that read it first (`on-first-use`). The same way of writing has two different moments.

**LB14.** `Probe` cannot enter this observation: `Probe` is not `Sendable` and cannot be held as a global constant under the Swift 6 rules; so the moment here is read from the order of the `print` lines, not from a count.

**LB15.** This measurement covers only one global constant and only one helper file; we do not measure other cases of the ordering rule (for example, several global constants whose setup depends on each other).

## Summary

- `let` is bound once, and a second assignment is rejected by the compiler; `var` can be reassigned.
- Type inference and explicit type annotation do not change the evaluation moment.
- Deferred initialization can be read only after the assignment; it gives `immediately` if the assignment comes before the use, and `on-first-use` if it is inside a wrapper around a use.
- Every `var` assignment evaluates its right-hand side again; the rule reads this as `on-every-use`.
- This lesson's moment is **immediately** (a constant evaluated on the line where it is defined or assigned).

## Next Step

We measured the assignment moments of constants and variables. The next lesson looks at the types of the values placed in them: when is a conversion between `Int`, `Double`, `Bool` and `String` made?
