2026-04-20
\[ \mathbb{E} \sum_{t=0}^T \beta^t y_t \]
where \(y_t = c\) if unemployed and \(y_t = w_t\) if employed
An unemployed worker with wage offer \(\bar w_j\) can reject the offer, receive \(c\), and draw a new offer next period
Or accept the offer, receive \(\bar w_j\), and be employed next period at wage \(\bar w_j\)
An employed worker with wage \(\bar w_j\) can quit, receive \(c\), and draw a new offer next period
Or continue to work, receive \(\bar w_j\), and be employed next period at wage \(\bar w_j\)
Insight
An employed worker deciding whether to keep wage \(\bar w_j\) faces the same problem as an unemployed worker who just received offer \(\bar w_j\).
structs\[ V_{1,j} = \max\left\{ \bar w_j,\; c \right\} \]
\[ Q_1 = \sum_s p_s V_{1,s} = p \cdot V_1 \]
\[ \bar w_s + \beta V_{1,s} \]
\[ c + \beta Q_1 \]
\[ V_{2,s} = \max\left\{ \bar w_s + \beta V_{1,s},\; c + \beta Q_1 \right\} \]
50-element Vector{Float64}:
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
8.451061224489795
⋮
16.634693877551022
16.99285714285714
17.351020408163265
17.709183673469386
18.067346938775508
18.425510204081633
18.783673469387757
19.14183673469388
19.5
\[ \boxed{V_{t,s} = \max\left\{ \bar w_s + \beta V_{t-1,s},\; c + \beta Q_{t-1} \right\}} \]
Solution Strategy
We solve backwards from the last period: compute \(V_1\), then \(V_2\), then \(V_3\), and so on.
T = 50 # number of periods the worker lives
S = length(w̄) # number of possible wages
V = zeros(T, S) # V[t,s] = value with t periods left, offer s
Q = zeros(T) # Q[t] = expected value of random offer
V[1, :] = max.(c, w̄) # base case: last period, just pick max
Q[1] = dot(p, V[1, :]) # expected value in last period
for t in 2:T # build up from t=2 to t=T
Vaccept = w̄ + β*V[t-1, :] # wage today + discounted continuation
Vreject = c + β*Q[t-1] # benefits today + discounted random offer
V[t, :] = max.(Vaccept, Vreject) # optimal choice for each wage
Q[t] = dot(p, V[t, :]) # update expected value
enditerateBellman: PseudocodeiterateBellman: Accept vs. RejectV_accept is a vector — one value for each wage offerV_reject is a scalar — same for all wages (the outside option)w̄ + β * V broadcasts element-wise: each wage \(\bar{w}_s\) plus the discounted continuation \(\beta V_s\)iterateBellman: Full Functionfunction iterateBellman(V, Q, β, p, w̄, c) # V, Q from next period
V_accept = w̄ .+ β .* V # w̄[s] + β*V[s] for each wage s
V_reject = c + β * Q # c + β*Q (same for all wages)
V_new = max.(V_accept, V_reject) # pick the better option
Q_new = dot(p, V_new) # expected value of random offer
C = V_accept .>= V_reject # 1 = accept, 0 = reject
return (V=V_new, Q=Q_new, C=C) # named tuple of results
enditerateBellman (generic function with 1 method)
Why Named Tuples?
With 3–4 return values, it’s easy to forget the order. Named tuples let you access results by name — no more guessing which output is which.
solveMcCall: PseudocodesolveMcCall: Base CaseV[1, :] stores the value function with 1 period left — simply \(\max(\bar{w}_s, c)\)Q[1] is the expected value of a random offer: \(Q_1 = p \cdot V_1\)solveMcCall: Full Functionfunction solveMcCall(β, p, w̄, c, T) # T = number of periods
S = length(w̄) # number of wage offers
V = zeros(T, S); Q = zeros(T); C = zeros(Int, T, S) # allocate storage
V[1, :] = max.(w̄, c) # base case: V with 1 period left
Q[1] = dot(p, V[1, :]) # expected value with 1 period left
C[1, :] = w̄ .>= c # accept if wage ≥ benefits
for t in 2:T # iterate from 2 to T periods
V[t, :], Q[t], C[t, :] = iterateBellman(V[t-1, :], Q[t-1], β, p, w̄, c)
end # uses previous period's solution
return (V=V, Q=Q, C=C) # return as named tuple
endsolveMcCall (generic function with 1 method)
\[ V_s = \max\left\{ \bar w_s + \beta V_s,\; c + \beta Q \right\} \]
where \(Q = p \cdot V\)
Fixed Point
The infinite horizon value function satisfies a fixed point equation: \(V\) appears on both sides.
V = max.(w̄, c) # initial guess for V
C = w̄ .>= c # initial guess for policy
Q = dot(p, V) # initial guess for Q
dist = 1.0 # initialize distance
while dist > 1e-10 # loop until convergence
V_new, Q_new, C = iterateBellman(V, Q, β, p, w̄, c) # one Bellman step
dist = norm(V - V_new, Inf) # max |V_new - V| across wages
V = V_new # replace old V with new
Q = Q_new # replace old Q with new
endWhy Does VFI Converge?
The Bellman operator is a contraction mapping with modulus \(\beta\).
Reservation Wage
The optimal policy is a threshold rule: accept any wage above a cutoff (the reservation wage) and reject wages below it.
Reservation wage: 7.98
solveMcCall (Infinite Horizon): PseudocodesolveMcCall (Infinite Horizon): The Convergence Loopnorm(V - V_new, Inf) computes the sup-norm: the largest absolute change across all wagessolveMcCall (Infinite Horizon): Full Functionfunction solveMcCall(β, p, w̄, c) # no T argument → infinite horizon
V = max.(w̄, c); C = w̄ .>= c; Q = dot(p, V) # initial guesses
dist = 1.0 # initialize distance
while dist > 1e-10 # iterate until convergence
V_new, Q_new, C = iterateBellman(V, Q, β, p, w̄, c) # Bellman step
dist = norm(V - V_new, Inf) # check max absolute difference
V = V_new; Q = Q_new # update V and Q
end
return (V=V, Q=Q, C=C) # return as named tuple
endsolveMcCall (generic function with 2 methods)
solveMcCall(β, p, w̄, c) — 4 separate argumentssolveMcCallFiring(β, p, w̄, c, α) — 5 argumentsAnalogy
McCallModel is like a blank form: “every McCall model needs a β, w̄, p, and c.” Creating an instance fills in the blanks with specific numbers.
@kwdef Macro@kwdef macro provides two useful features:
(; ...), extract fields into local variablesMcCallModelMcCallModel
@kwdef lets every field have a default valueMcCallModel(0.95, 50, [1.0, 1.183673469387755, 1.3673469387755102, 1.5510204081632653, 1.7346938775510203, 1.9183673469387754, 2.1020408163265305, 2.2857142857142856, 2.4693877551020407, 2.6530612244897958 … 8.346938775510203, 8.53061224489796, 8.714285714285714, 8.89795918367347, 9.081632653061224, 9.26530612244898, 9.448979591836736, 9.63265306122449, 9.816326530612246, 10.0], [0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02 … 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], 3.0, 0.0)
McCallModel(0.95, 50, [1.0, 1.183673469387755, 1.3673469387755102, 1.5510204081632653, 1.7346938775510203, 1.9183673469387754, 2.1020408163265305, 2.2857142857142856, 2.4693877551020407, 2.6530612244897958 … 8.346938775510203, 8.53061224489796, 8.714285714285714, 8.89795918367347, 9.081632653061224, 9.26530612244898, 9.448979591836736, 9.63265306122449, 9.816326530612246, 10.0], [0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02 … 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], 5.0, 0.0)
McCallModel(0.99, 50, [1.0, 1.183673469387755, 1.3673469387755102, 1.5510204081632653, 1.7346938775510203, 1.9183673469387754, 2.1020408163265305, 2.2857142857142856, 2.4693877551020407, 2.6530612244897958 … 8.346938775510203, 8.53061224489796, 8.714285714285714, 8.89795918367347, 9.081632653061224, 9.26530612244898, 9.448979591836736, 9.63265306122449, 9.816326530612246, 10.0], [0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02 … 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], 4.0, 0.0)
Why Destructuring?
Instead of writing m.β, m.p, m.w̄, m.c everywhere in our math, destructuring gives us clean local variable names — the code looks the same as before.
iterateBellman with a Structfunction iterateBellman(m::McCallModel, V, Q) # V, Q from next period + model
(; β, p, w̄, c) = m # extract parameters
V_accept = w̄ + β * V # w̄[s] + β*V[s] for each wage s
V_reject = c + β * Q # c + β*Q (same for all wages)
V_new = max.(V_accept, V_reject) # pick the better option
Q_new = dot(p, V_new) # expected value of random offer
C = V_accept .>= V_reject # 1 = accept, 0 = reject
return (V=V_new, Q=Q_new, C=C) # named tuple of results
enditerateBellman (generic function with 2 methods)
solveMcCall with a Structfunction solveMcCall(m::McCallModel) # takes a single McCallModel
(; β, p, w̄, c) = m # extract parameters
V = max.(w̄, c); C = w̄ .>= c; Q = dot(p, V)
dist = 1.0
while dist > 1e-10
V_new, Q_new, C = iterateBellman(m, V, Q) # pass the model
dist = norm(V - V_new, Inf)
V = V_new; Q = Q_new
end
return (V=V, Q=Q, C=C) # named tuple
endsolveMcCall (generic function with 3 methods)
(V = [158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073, 158.24988988232073 … 166.93877550863462, 170.61224489635495, 174.28571428407582, 177.9591836717961, 181.63265305951697, 185.3061224472373, 188.97959183495811, 192.65306122267842, 196.32653061039926, 199.9999999981196], Q = 163.42093671832137, C = Bool[0, 0, 0, 0, 0, 0, 0, 0, 0, 0 … 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
5-element Vector{Float64}:
158.24988988232073
158.24988988232073
158.24988988232073
158.24988988232073
158.24988988232073
Julia’s Multiple Dispatch
Julia picks which solveMcCall to call based on the argument type:
solveMcCall(m) where m::McCallModel → struct versionsolveMcCall(β, p, w̄, c) → original versionBoth work! Julia dispatches to the right method automatically.
\[ h = p_1 C_1 + p_2 C_2 + \cdots + p_S C_S = p \cdot C \]
\[ \mathbb{E}[\text{duration}] = \frac{1}{h} \]
cvalues = LinRange(0, 5, 100) # grid of c values to try
hvalues = zeros(length(cvalues)) # store hazard rate for each c
for i in 1:length(cvalues) # loop over each c
sol = solveMcCall(McCallModel(c=cvalues[i])) # create model, solve
hvalues[i] = dot(p, sol.C) # compute hazard rate p⋅C
end
plot(cvalues, hvalues, linewidth=2, label="Hazard Rate", legend=:topright)
xlabel!("Unemployment Benefits (c)") # label axes
ylabel!("Hazard Rate")\[ U_t = c + \beta Q_{t-1} \]
\[ \bar w_s + \beta\left[(1-\alpha) V_{t-1,s} + \alpha U_{t-1}\right] \]
\[ \boxed{V_{t,s} = \max\left\{ \bar w_s + \beta\left[(1-\alpha) V_{t-1,s} + \alpha U_{t-1}\right],\; U_t \right\}} \]
Effect of Firing
Firing risk reduces the value of accepting a job, since employment is no longer permanent.
function iterateBellmanFiring(m::McCallModel, V, Q, U) # model first
(; β, p, w̄, c, α) = m # extract all parameters
V_accept = w̄ + β * ((1-α)*V .+ α*U) # keep job w.p. 1-α, fired w.p. α
U_new = c + β * Q # unemployment: benefits + random offer
V_new = max.(V_accept, U_new) # pick the better option
Q_new = dot(p, V_new) # expected value of random offer
C = V_accept .>= U_new # 1 = accept, 0 = reject
return (V=V_new, Q=Q_new, U=U_new, C=C) # named tuple with U
enditerateBellmanFiring (generic function with 1 method)
iterateBellmanfunction solveMcCallFiring(m::McCallModel) # takes a McCallModel with α > 0
(; β, p, w̄, c, α) = m # extract all parameters
V = max.(w̄, c); C = w̄ .>= c # initial guess for V, C
Q = dot(p, V); U = c # initial guess for Q, U
dist = 1.0 # initialize distance
while dist > 1e-10 # iterate until convergence
V_new, Q_new, U_new, C = iterateBellmanFiring(m, V, Q, U) # model first
dist = norm(V - V_new, Inf) # max absolute change in V
V = V_new; Q = Q_new; U = U_new # update all values
end
return (V=V, Q=Q, U=U, C=C) # return as named tuple
endsolveMcCallFiring (generic function with 1 method)
Mean under p: 5.500000000000001
Mean under p₂: 5.5
Option Value
The worker is strictly better off with more variance. The option to reject bad offers means the worker benefits from upside risk without fully bearing downside risk.
Concepts
Julia Tools
max.() for element-wise maxdot() for expected valuesnorm() for convergence checks@kwdef, destructuring (; ...)Remember
The McCall model shows that search is valuable: a rational worker will reject low offers and wait for better ones, especially when the future is long (\(T\) large) and they are patient (\(\beta\) high).
EC 410 | University of Oregon