Skip to content

Instantly share code, notes, and snippets.

@ickc
Created March 3, 2026 15:40
Show Gist options
  • Select an option

  • Save ickc/937210e46b58fe9ba309a5d7c522c3a9 to your computer and use it in GitHub Desktop.

Select an option

Save ickc/937210e46b58fe9ba309a5d7c522c3a9 to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
# %% [markdown]
# # NTuple invariance vs. tuple covariance
#
# Illustrates why `NTuple{N, <:AbstractFloat}` and `NTuple{N, AbstractFloat}`
# mean very different things.
# %%
# --- helper ---
check(label, val) = println(label, ": ", val)
# %% [markdown]
# ## 1. `NTuple{N, <:AbstractFloat}` — invariant TypeVar
#
# `NTuple{2, <:AbstractFloat}` expands to `NTuple{2, T} where T <: AbstractFloat`.
# Julia must solve `T` to a **single** concrete type that satisfies every element,
# so both elements must share exactly the same type.
# %%
# Both Float64 → T = Float64 ✓
check("(1.0, 2.0) <: NTuple{2,<:AbstractFloat}",
(1.0, 2.0) isa NTuple{2, <:AbstractFloat})
# Both Float32 → T = Float32 ✓
check("(1f0, 2f0) <: NTuple{2,<:AbstractFloat}",
(1.0f0, 2.0f0) isa NTuple{2, <:AbstractFloat})
# Float64 + Float32 → no single T satisfies both ✗
check("(1.0, 1f0) <: NTuple{2,<:AbstractFloat}",
(1.0, 1.0f0) isa NTuple{2, <:AbstractFloat})
# %% [markdown]
# ## 2. `NTuple{N, AbstractFloat}` — covariant tuple
#
# `NTuple{2, AbstractFloat}` means `Tuple{AbstractFloat, AbstractFloat}`.
# Julia tuples are **covariant**: `Tuple{A,B} <: Tuple{P,Q}` holds whenever
# `A <: P` and `B <: Q` are satisfied *independently* for each position.
# `A` and `B` do **not** need to be the same type.
# %%
# Both Float64 → Float64 <: AbstractFloat ✓
check("(1.0, 2.0) <: NTuple{2,AbstractFloat}",
(1.0, 2.0) isa NTuple{2, AbstractFloat})
# Float64 + Float32 → each position checked independently ✓
check("(1.0, 1f0) <: NTuple{2,AbstractFloat}",
(1.0, 1.0f0) isa NTuple{2, AbstractFloat})
# Float64 + Int (not <: AbstractFloat) ✗
check("(1.0, 1) <: NTuple{2,AbstractFloat}",
(1.0, 1) isa NTuple{2, AbstractFloat})
# %% [markdown]
# ## 3. Type-level check (dispatch)
#
# The same distinction appears in method dispatch / `<:` on types.
# %%
# NTuple{2,<:AbstractFloat} requires a common T
check("Tuple{Float64,Float64} <: NTuple{2,<:AbstractFloat}",
Tuple{Float64, Float64} <: NTuple{2, <:AbstractFloat})
check("Tuple{Float64,Float32} <: NTuple{2,<:AbstractFloat}",
Tuple{Float64, Float32} <: NTuple{2, <:AbstractFloat})
println()
# NTuple{2,AbstractFloat} only needs each position to be <: AbstractFloat
check("Tuple{Float64,Float64} <: NTuple{2,AbstractFloat}",
Tuple{Float64, Float64} <: NTuple{2, AbstractFloat})
check("Tuple{Float64,Float32} <: NTuple{2,AbstractFloat}",
Tuple{Float64, Float32} <: NTuple{2, AbstractFloat})
# %% [markdown]
# ### Summary
#
# | Syntax | Constraint |
# |--------|-----------|
# | `NTuple{N, <:AbstractFloat}` | all N elements must be the **same** subtype T |
# | `NTuple{N, AbstractFloat}` | each element only needs to satisfy `<: AbstractFloat` independently |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment