C.W.K.
Stream
Lesson 03 of 05 · published

Functional Updates: From Modify to Return

~8 min · purity, jax, tutorial

Level 0Curious
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

The biggest mental shift for JAX newcomers is moving from "modify state" to "return new state." In imperative programming, you modify variables in place. In functional programming, you create new values and return them.

The Functional Update Pattern

import jax.numpy as jnp

# IMPERATIVE (NumPy) style — won't work in JAX
# array[i] = value
# dict[key] = value
# object.attribute = value

# FUNCTIONAL (JAX) style — creates new values
# new_array = array.at[i].set(value)
# new_dict = {**dict, key: value}
# new_params = params._replace(attribute=value)  # NamedTuple

.at[] — The Swiss Army Knife

import jax.numpy as jnp

x = jnp.zeros((5, 5))

# Set values at specific positions
x = x.at[0, 0].set(1.0)
x = x.at[2, 3].set(5.0)
x = x.at[4, 4].set(1.0)

# Increment values
counts = jnp.zeros(10, dtype=jnp.int32)
data = jnp.array([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])
# Build a histogram — add 1 at each index
histogram = counts.at[data].add(1)
print(histogram)  # [0 2 1 2 1 2 0 0 0 1]
# Note: duplicate indices accumulate (both 1s add, both 3s add, etc.)

# Scatter operations
indices = jnp.array([0, 2, 4])
values = jnp.array([10.0, 20.0, 30.0])
x = jnp.zeros(6)
x = x.at[indices].set(values)
print(x)  # [10.  0. 20.  0. 30.  0.]

jnp.where: Conditional Updates Without Branching

import jax.numpy as jnp

x = jnp.array([-3.0, -1.0, 0.0, 2.0, 5.0])

# ReLU: replace negatives with 0
relu = jnp.where(x > 0, x, 0.0)
print(relu)  # [0. 0. 0. 2. 5.]

# Clamp between -1 and 1
clamped = jnp.where(x > 1, 1.0, jnp.where(x < -1, -1.0, x))
print(clamped)  # [-1. -1. 0. 1. 1.]

# Conditional based on another array
mask = jnp.array([True, False, True, False, True])
result = jnp.where(mask, x, 0.0)
print(result)  # [-3. 0. 0. 0. 5.]

The Pattern: Carry State Through Returns

import jax
import jax.numpy as jnp

# IMPERATIVE: modify accumulator in a loop
def imperative_cumsum(arr):
    result = []
    total = 0
    for x in arr:
        total += x
        result.append(total)
    return result

# FUNCTIONAL: use jax.lax.scan (functional loop)
def functional_cumsum(arr):
    def step(carry, x):
        new_carry = carry + x
        return new_carry, new_carry  # (next_carry, output)

    _, cumulative = jax.lax.scan(step, 0.0, arr)
    return cumulative

x = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(functional_cumsum(x))  # [1. 3. 6. 10. 15.]

⚠️ Pure Function Check

Ask yourself before every function: "If I call this function twice with the same arguments, will I get the same result? Does it change anything outside itself?" If the answer to either question is "no," your function is impure and may cause problems with JAX transformations.

Code

import jax.numpy as jnp

# IMPERATIVE (NumPy) style — won't work in JAX
# array[i] = value
# dict[key] = value
# object.attribute = value

# FUNCTIONAL (JAX) style — creates new values
# new_array = array.at[i].set(value)
# new_dict = {**dict, key: value}
# new_params = params._replace(attribute=value)  # NamedTuple
import jax.numpy as jnp

x = jnp.zeros((5, 5))

# Set values at specific positions
x = x.at[0, 0].set(1.0)
x = x.at[2, 3].set(5.0)
x = x.at[4, 4].set(1.0)

# Increment values
counts = jnp.zeros(10, dtype=jnp.int32)
data = jnp.array([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])
# Build a histogram — add 1 at each index
histogram = counts.at[data].add(1)
print(histogram)  # [0 2 1 2 1 2 0 0 0 1]
# Note: duplicate indices accumulate (both 1s add, both 3s add, etc.)

# Scatter operations
indices = jnp.array([0, 2, 4])
values = jnp.array([10.0, 20.0, 30.0])
x = jnp.zeros(6)
x = x.at[indices].set(values)
print(x)  # [10.  0. 20.  0. 30.  0.]
import jax.numpy as jnp

x = jnp.array([-3.0, -1.0, 0.0, 2.0, 5.0])

# ReLU: replace negatives with 0
relu = jnp.where(x > 0, x, 0.0)
print(relu)  # [0. 0. 0. 2. 5.]

# Clamp between -1 and 1
clamped = jnp.where(x > 1, 1.0, jnp.where(x < -1, -1.0, x))
print(clamped)  # [-1. -1. 0. 1. 1.]

# Conditional based on another array
mask = jnp.array([True, False, True, False, True])
result = jnp.where(mask, x, 0.0)
print(result)  # [-3. 0. 0. 0. 5.]
import jax
import jax.numpy as jnp

# IMPERATIVE: modify accumulator in a loop
def imperative_cumsum(arr):
    result = []
    total = 0
    for x in arr:
        total += x
        result.append(total)
    return result

# FUNCTIONAL: use jax.lax.scan (functional loop)
def functional_cumsum(arr):
    def step(carry, x):
        new_carry = carry + x
        return new_carry, new_carry  # (next_carry, output)

    _, cumulative = jax.lax.scan(step, 0.0, arr)
    return cumulative

x = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(functional_cumsum(x))  # [1. 3. 6. 10. 15.]

External links

Exercise

Take a 4×4 grid. Apply 10 random updates using .at[i,j].set(v). Then do the same updates using a NumPy-style mutating loop on a copy. Compare output equality and timing. Note the readability difference.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.