Skip to content

add shuffle(::NTuple) to Random #56906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7936a9f
add `shuffle(::NTuple)` to `Random`
nsajko Dec 26, 2024
f3fb9f9
rely on `shuffle!` and on escape analysis instead of on recursion
nsajko May 9, 2025
dc162c3
bugfix: use provided pseudorandom generator
nsajko May 9, 2025
2eee6e6
simplify using `randperm!` as per rfourqet
nsajko May 9, 2025
8a8efff
delete unnecessary branch
nsajko May 9, 2025
fcec466
reindent
nsajko May 9, 2025
9e9c1a5
add news
nsajko May 9, 2025
ed3e09a
update doc string, and apply it to the function instead of to a method
nsajko May 9, 2025
5b50ae3
use narrower type when possible as per rfourqet
nsajko May 9, 2025
90f1d2d
de-hardcode `typemax(UInt8)`
nsajko May 9, 2025
6e80b0d
safer
nsajko May 9, 2025
18ededb
use `shuffle!` instead of `randperm!` again
nsajko May 9, 2025
c6c3997
relax one-arg method
nsajko May 9, 2025
4ab6f86
simpler dispatch constraint
nsajko May 9, 2025
d7191ef
delete now unnecessary method
nsajko May 9, 2025
8f3800f
delete extraneous newline
nsajko May 9, 2025
ba162dc
omit no alloc test set
nsajko May 9, 2025
912ec6c
fix dispatch constraint: restrict to `NTuple`
nsajko May 12, 2025
35feb2d
add compat annotation to doc string
nsajko May 12, 2025
bed23af
adjust "not identity at least once" test
nsajko May 31, 2025
9944d1c
expand comment
nsajko May 31, 2025
6ebd824
simplify syntax, less `let`, etc
nsajko May 31, 2025
d1ea278
use `randperm!` again, as it supports `Memory` now
nsajko Jun 10, 2025
f9dd73d
restrict the one-arg method from `Tuple` to `NTuple`
nsajko Jun 10, 2025
157ad7a
Merge branch 'master' into shuffle_ntuple
nsajko Jun 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Standard library changes

* `randperm!` and `randcycle!` now support non-`Array` `AbstractArray` inputs, assuming they are mutable and their indices are one-based ([#58596]).

* `shuffle` now may take an argument of `NTuple` value ([#56906]).

#### REPL

* The display of `AbstractChar`s in the main REPL mode now includes LaTeX input information like what is shown in help mode ([#58181]).
Expand Down
37 changes: 35 additions & 2 deletions stdlib/Random/src/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,34 @@ ltm52(n::Int, mask::Int=nextpow(2, n)-1) = LessThan(n-1, Masked(mask, UInt52Raw(

## shuffle & shuffle!

function shuffle(rng::AbstractRNG, tup::NTuple{N}) where {N}
# `@inline` and `@inbounds` are here to help escape analysis eliminate the `Memory` allocation
#
# * `@inline` might be necessary because escape analysis relies on everything
# touching the `Memory` being inlined because there's no interprocedural escape
# analysis yet, relevant WIP PR: https://github.com/JuliaLang/julia/pull/56849
#
# * `@inbounds` might be necessary because escape analysis requires any throws of
# `BoundsError` to be eliminated as dead code, because `BoundsError` stores the
# array itself, making the throw escape the array from the function, relevant
# WIP PR: https://github.com/JuliaLang/julia/pull/56167
@inline let
# use a narrow integer type to save stack space and prevent heap allocation
Ind = if N ≤ typemax(UInt8)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for which sizes of tuples does this Ind help? is there a cutoff?

Copy link
Contributor Author

@nsajko nsajko Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there's a cutoff. The transformation of Memory heap allocations to stack allocations currently only works for Memory with small statically-known size. See PR #56847.

UInt8
elseif N ≤ typemax(UInt16)
UInt16
else
UInt
end
mem = @inbounds randperm!(rng, Memory{Ind}(undef, N))
function closure(i::Int)
@inbounds tup[mem[i]]
end
ntuple(closure, Val{N}())::typeof(tup)
end
end

"""
shuffle!([rng=default_rng(),] v::AbstractArray)

Expand Down Expand Up @@ -238,13 +266,16 @@ end
shuffle!(a::AbstractArray) = shuffle!(default_rng(), a)

"""
shuffle([rng=default_rng(),] v::AbstractArray)
shuffle([rng=default_rng(),] v::Union{NTuple,AbstractArray})

Return a randomly permuted copy of `v`. The optional `rng` argument specifies a random
number generator (see [Random Numbers](@ref)).
To permute `v` in-place, see [`shuffle!`](@ref). To obtain randomly permuted
indices, see [`randperm`](@ref).

!!! compat "Julia 1.13"
Shuffling an `NTuple` value requires Julia v1.13 or above.

# Examples
```jldoctest
julia> shuffle(Xoshiro(123), Vector(1:10))
Expand All @@ -261,8 +292,10 @@ julia> shuffle(Xoshiro(123), Vector(1:10))
7
```
"""
function shuffle end

shuffle(r::AbstractRNG, a::AbstractArray) = shuffle!(r, copymutable(a))
shuffle(a::AbstractArray) = shuffle(default_rng(), a)
shuffle(a::Union{NTuple, AbstractArray}) = shuffle(default_rng(), a)

shuffle(r::AbstractRNG, a::Base.OneTo) = randperm(r, last(a))

Expand Down
17 changes: 17 additions & 0 deletions stdlib/Random/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,23 @@ end
@test maximum(m) <= 0.106
end

@testset "`shuffle(::NTuple)`" begin
@testset "sorted" begin
for n ∈ 0:20
tup = ntuple(identity, n)
@test tup === sort(@inferred shuffle(tup))
end
end
@testset "not identity" begin
function shuffle_is_identity()
tup = ntuple(identity, 9)
tup === shuffle(tup)
end
# shuffling may behave as the identity sometimes, but if it doesn't manage to actually reorder some of the elements at least once, something is wrong
@test any((_ -> !shuffle_is_identity()), 1:1000000)
end
end

# issue #42752
# test that running finalizers that launch tasks doesn't change RNG stream
function f42752(do_gc::Bool, cell = (()->Any[[]])())
Expand Down