diff --git a/NEWS.md b/NEWS.md index 1e4e24b67a4db..dcf2b13c12214 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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]). diff --git a/stdlib/Random/src/misc.jl b/stdlib/Random/src/misc.jl index 0f113e8153f04..395137b50abfd 100644 --- a/stdlib/Random/src/misc.jl +++ b/stdlib/Random/src/misc.jl @@ -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) + 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) @@ -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)) @@ -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)) diff --git a/stdlib/Random/test/runtests.jl b/stdlib/Random/test/runtests.jl index e929cd2d8a517..6403fbfe958c3 100644 --- a/stdlib/Random/test/runtests.jl +++ b/stdlib/Random/test/runtests.jl @@ -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[[]])())