Skip to content

Commit c75f73d

Browse files
committed
support sorting tuples
Uses merge sort, as an obvious choice for a stable sort of tuples. A recursive data structure of singleton type, representing Peano natural numbers, is used to help with splitting a tuple into two halves in the merge sort. An alternative design would use a reference tuple, but this would require relying on `tail`, which seems more harsh on the compiler. With the recursive datastructure the predecessor operation and the successor operation are both trivial. Allows inference to preserve inferred element type even when tuple length is not known. Follow-up PRs may add further improvements, such as the ability to select an unstable sorting algorithm. The added file, typedomainnumbers.jl is not specific to sorting, thus making it a separate file. Xref JuliaLang#55571. Fixes JuliaLang#54489
1 parent ca2d6aa commit c75f73d

File tree

8 files changed

+311
-6
lines changed

8 files changed

+311
-6
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ New library features
119119
* `Base.require_one_based_indexing` and `Base.has_offset_axes` are now public ([#56196])
120120
* New `ltruncate`, `rtruncate` and `ctruncate` functions for truncating strings to text width, accounting for char widths ([#55351])
121121
* `isless` (and thus `cmp`, sorting, etc.) is now supported for zero-dimensional `AbstractArray`s ([#55772])
122+
* `sort` now sorts tuples (#56425)
122123

123124
Standard library changes
124125
------------------------

base/Base.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ include("cartesian.jl")
106106
using .Cartesian
107107
include("multidimensional.jl")
108108

109+
include("typedomainnumbers.jl")
110+
109111
include("broadcast.jl")
110112
using .Broadcast
111113
using .Broadcast: broadcasted, broadcasted_kwsyntax, materialize, materialize!,

base/sort.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,82 @@ julia> v
17361736
"""
17371737
sort(v::AbstractVector; kws...) = sort!(copymutable(v); kws...)
17381738

1739+
module _SortTupleStable
1740+
using
1741+
Base._TypeDomainNumbers.PositiveIntegers, Base._TypeDomainNumbers.IntegersGreaterThanOne,
1742+
Base._TypeDomainNumberTupleUtils, Base._TupleTypeByLength
1743+
using Base: tail
1744+
using Base.Order: Ordering, lt
1745+
function merge_recursive((@nospecialize ord::Ordering), a::Tuple, b::Tuple)
1746+
if a isa Tuple1OrMore
1747+
a
1748+
else
1749+
b
1750+
end
1751+
end
1752+
function merge_recursive(ord::Ordering, a::Tuple1OrMore, b::Tuple1OrMore)
1753+
l = first(a)
1754+
r = first(b)
1755+
x = tail(a)
1756+
y = tail(b)
1757+
if lt(ord, r, l)
1758+
let rec = merge_recursive(ord, a, y)
1759+
(r, rec...)
1760+
end
1761+
else
1762+
let rec = merge_recursive(ord, x, b)
1763+
(l, rec...)
1764+
end
1765+
end
1766+
end
1767+
function merge_nontrivial(ord::Ordering, a::Tuple1OrMore, b::Tuple1OrMore)
1768+
merge_recursive(ord, a, b)
1769+
end
1770+
function sort_recursive((@nospecialize ord::Ordering), @nospecialize tup::Tuple{Any})
1771+
tup
1772+
end
1773+
function sort_recursive(ord::Ordering, tup::Tuple2OrMore)
1774+
(tup_l, tup_r) = split_tuple_into_halves(tup)
1775+
sorted_l = sort_recursive(ord, tup_l)
1776+
sorted_r = sort_recursive(ord, tup_r)
1777+
merge_nontrivial(ord, sorted_l, sorted_r)
1778+
end
1779+
function sort_tuple_stable_2_or_more(ord::Ordering, tup::Tuple2OrMore)
1780+
sort_recursive(ord, tup)
1781+
end
1782+
function sort_tuple_array_fallback(ord::Ordering, tup::Tuple2OrMore)
1783+
vec = if tup isa NTuple
1784+
[tup...]
1785+
else
1786+
Any[tup...]
1787+
end
1788+
sort!(vec; order = ord)
1789+
(vec...,)
1790+
end
1791+
function sort_tuple_stable((@nospecialize ord::Ordering), @nospecialize tup::Tuple)
1792+
if tup isa Tuple2OrMore
1793+
if tup isa Tuple32OrMore
1794+
sort_tuple_array_fallback(ord, tup)
1795+
else
1796+
sort_tuple_stable_2_or_more(ord, tup)
1797+
end
1798+
else
1799+
tup
1800+
end
1801+
end
1802+
end
1803+
1804+
function sort(
1805+
tup::Tuple;
1806+
lt = isless,
1807+
by = identity,
1808+
rev::Union{Nothing, Bool} = nothing,
1809+
order::Ordering = Forward,
1810+
)
1811+
o = ord(lt, by, rev, order)
1812+
_SortTupleStable.sort_tuple_stable(o, tup)
1813+
end
1814+
17391815
## partialsortperm: the permutation to sort the first k elements of an array ##
17401816

17411817
"""

base/tuple.jl

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3+
module _TupleTypeByLength
4+
export Tuple1OrMore, Tuple2OrMore, Tuple3OrMore, Tuple4OrMore, Tuple32OrMore
5+
const Tuple1OrMore = Tuple{Any, Vararg}
6+
const Tuple2OrMore = Tuple{Any, Any, Vararg}
7+
const Tuple3OrMore = Tuple{Any, Any, Any, Vararg}
8+
const Tuple4OrMore = Tuple{Any, Any, Any, Any, Vararg}
9+
const Tuple32OrMore = Tuple{
10+
Any, Any, Any, Any, Any, Any, Any, Any,
11+
Any, Any, Any, Any, Any, Any, Any, Any,
12+
Any, Any, Any, Any, Any, Any, Any, Any,
13+
Any, Any, Any, Any, Any, Any, Any, Any,
14+
Vararg{Any, N},
15+
} where {N}
16+
end
17+
318
# Document NTuple here where we have everything needed for the doc system
419
"""
520
NTuple{N, T}
@@ -358,11 +373,7 @@ map(f, t::Tuple{Any, Any}) = (@inline; (f(t[1]), f(t[2])))
358373
map(f, t::Tuple{Any, Any, Any}) = (@inline; (f(t[1]), f(t[2]), f(t[3])))
359374
map(f, t::Tuple) = (@inline; (f(t[1]), map(f,tail(t))...))
360375
# stop inlining after some number of arguments to avoid code blowup
361-
const Any32{N} = Tuple{Any,Any,Any,Any,Any,Any,Any,Any,
362-
Any,Any,Any,Any,Any,Any,Any,Any,
363-
Any,Any,Any,Any,Any,Any,Any,Any,
364-
Any,Any,Any,Any,Any,Any,Any,Any,
365-
Vararg{Any,N}}
376+
const Any32{N} = _TupleTypeByLength.Tuple32OrMore
366377
const All32{T,N} = Tuple{T,T,T,T,T,T,T,T,
367378
T,T,T,T,T,T,T,T,
368379
T,T,T,T,T,T,T,T,

base/typedomainnumbers.jl

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
# Adapted from the TypeDomainNaturalNumbers.jl package.
4+
module _TypeDomainNumbers
5+
module Zeros
6+
export Zero
7+
struct Zero end
8+
end
9+
10+
module PositiveIntegers
11+
module RecursiveStep
12+
using ...Zeros
13+
export recursive_step
14+
function recursive_step(@nospecialize t::Type)
15+
Union{Zero, t}
16+
end
17+
end
18+
module UpperBounds
19+
using ..RecursiveStep
20+
abstract type A end
21+
abstract type B{P <: recursive_step(A)} <: A end
22+
abstract type C{P <: recursive_step(B)} <: B{P} end
23+
abstract type D{P <: recursive_step(C)} <: C{P} end
24+
end
25+
using .RecursiveStep
26+
const PositiveIntegerUpperBound = UpperBounds.A
27+
const PositiveIntegerUpperBoundTighter = UpperBounds.D
28+
export
29+
natural_successor, natural_predecessor,
30+
NonnegativeInteger, NonnegativeIntegerUpperBound,
31+
PositiveInteger, PositiveIntegerUpperBound
32+
struct PositiveInteger{
33+
Predecessor <: recursive_step(PositiveIntegerUpperBoundTighter),
34+
} <: PositiveIntegerUpperBoundTighter{Predecessor}
35+
predecessor::Predecessor
36+
global const NonnegativeInteger = recursive_step(PositiveInteger)
37+
global const NonnegativeIntegerUpperBound = recursive_step(PositiveIntegerUpperBound)
38+
global function natural_successor(p::P) where {P <: NonnegativeInteger}
39+
new{P}(p)
40+
end
41+
end
42+
function natural_predecessor(@nospecialize o::PositiveInteger)
43+
getfield(o, :predecessor) # avoid specializing `getproperty` for each number
44+
end
45+
end
46+
47+
module IntegersGreaterThanOne
48+
using ..PositiveIntegers
49+
export
50+
IntegerGreaterThanOne, IntegerGreaterThanOneUpperBound,
51+
natural_predecessor_predecessor
52+
const IntegerGreaterThanOne = let t = PositiveInteger
53+
t{P} where {P <: t}
54+
end
55+
const IntegerGreaterThanOneUpperBound = let t = PositiveIntegerUpperBound
56+
PositiveIntegers.UpperBounds.B{P} where {P <: t}
57+
end
58+
function natural_predecessor_predecessor(@nospecialize x::IntegerGreaterThanOne)
59+
natural_predecessor(natural_predecessor(x))
60+
end
61+
end
62+
63+
module Constants
64+
using ..Zeros, ..PositiveIntegers
65+
export n0, n1
66+
const n0 = Zero()
67+
const n1 = natural_successor(n0)
68+
end
69+
70+
module Utils
71+
using ..PositiveIntegers, ..IntegersGreaterThanOne, ..Constants
72+
using Base: @assume_effects
73+
export half_floor, half_ceiling
74+
@assume_effects :foldable :nothrow function half_floor(@nospecialize m::NonnegativeInteger)
75+
if m isa IntegerGreaterThanOneUpperBound
76+
let n = natural_predecessor_predecessor(m), rec = half_floor(n)
77+
natural_successor(rec)
78+
end
79+
else
80+
n0
81+
end
82+
end
83+
@assume_effects :foldable :nothrow function half_ceiling(@nospecialize m::NonnegativeInteger)
84+
if m isa IntegerGreaterThanOneUpperBound
85+
let n = natural_predecessor_predecessor(m), rec = half_ceiling(n)
86+
natural_successor(rec)
87+
end
88+
else
89+
if m isa PositiveIntegerUpperBound
90+
n1
91+
else
92+
n0
93+
end
94+
end
95+
end
96+
end
97+
end
98+
99+
module _TypeDomainNumberTupleUtils
100+
using
101+
.._TypeDomainNumbers.PositiveIntegers, .._TypeDomainNumbers.IntegersGreaterThanOne,
102+
.._TypeDomainNumbers.Constants, .._TypeDomainNumbers.Utils, .._TupleTypeByLength
103+
using Base: @assume_effects, front, tail
104+
export tuple_type_domain_length, split_tuple_into_halves, skip_from_front, skip_from_tail
105+
@assume_effects :foldable :nothrow function tuple_type_domain_length(@nospecialize tup::Tuple)
106+
if tup isa Tuple1OrMore
107+
let t = tail(tup), rec = tuple_type_domain_length(t)
108+
natural_successor(rec)
109+
end
110+
else
111+
n0
112+
end
113+
end
114+
@assume_effects :foldable function skip_from_front((@nospecialize tup::Tuple), @nospecialize skip_count::NonnegativeInteger)
115+
if skip_count isa PositiveIntegerUpperBound
116+
let cm1 = natural_predecessor(skip_count), t = tail(tup)
117+
@inline skip_from_front(t, cm1)
118+
end
119+
else
120+
tup
121+
end
122+
end
123+
@assume_effects :foldable function skip_from_tail((@nospecialize tup::Tuple), @nospecialize skip_count::NonnegativeInteger)
124+
if skip_count isa PositiveIntegerUpperBound
125+
let cm1 = natural_predecessor(skip_count), t = front(tup)
126+
@inline skip_from_tail(t, cm1)
127+
end
128+
else
129+
tup
130+
end
131+
end
132+
function split_tuple_into_halves(@nospecialize tup::Tuple)
133+
len = tuple_type_domain_length(tup)
134+
len_l = half_floor(len)
135+
len_r = half_ceiling(len)
136+
tup_l = skip_from_tail(tup, len_r)
137+
tup_r = skip_from_front(tup, len_l)
138+
(tup_l, tup_r)
139+
end
140+
end

test/choosetests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const TESTNAMES = [
1515
"bitarray", "copy", "math", "fastmath", "functional", "iterators",
1616
"operators", "ordering", "path", "ccall", "parse", "loading", "gmp",
1717
"sorting", "spawn", "backtrace", "exceptions",
18-
"file", "read", "version", "namedtuple",
18+
"file", "read", "version", "namedtuple", "typedomainnumbers",
1919
"mpfr", "broadcast", "complex",
2020
"floatapprox", "stdlib", "reflection", "regex", "float16",
2121
"combinatorics", "sysinfo", "env", "rounding", "ranges", "mod2pi",

test/sorting.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,50 @@ end
9292
end
9393
@test sort(1:2000, by=x->x÷100, rev=true) == sort(1:2000, by=x->-x÷100) ==
9494
vcat(2000, (x:x+99 for x in 1900:-100:100)..., 1:99)
95+
@testset "tuples" begin
96+
tup = Tuple(0:9)
97+
@test tup === sort(tup; by = _ -> 0)
98+
@test (0, 2, 4, 6, 8, 1, 3, 5, 7, 9) === sort(tup; by = x -> isodd(x))
99+
@test (1, 3, 5, 7, 9, 0, 2, 4, 6, 8) === sort(tup; by = x -> iseven(x))
100+
end
101+
end
102+
103+
@testset "tuple sorting" begin
104+
max_unrolled_length = 31
105+
@testset "correctness" begin
106+
tup = Tuple(0:9)
107+
tup_rev = reverse(tup)
108+
@test tup === @inferred sort(tup)
109+
@test tup === sort(tup; rev = false)
110+
@test tup_rev === sort(tup; rev = true)
111+
@test tup_rev === sort(tup; lt = >)
112+
end
113+
@testset "inference" begin
114+
known_length = (Tuple{Vararg{Int, max_unrolled_length}}, Tuple{Vararg{Float64, max_unrolled_length}})
115+
unknown_length = (Tuple{Vararg{Int}}, Tuple{Vararg{Float64}})
116+
for Tup (known_length..., unknown_length...)
117+
@test Tup == Base.infer_return_type(sort, Tuple{Tup})
118+
end
119+
for Tup (known_length...,)
120+
@test Core.Compiler.is_foldable(Base.infer_effects(sort, Tuple{Tup}))
121+
end
122+
end
123+
@testset "alloc" begin
124+
function test_zero_allocated(tup::Tuple)
125+
@test iszero(@allocated sort(tup))
126+
end
127+
test_zero_allocated(ntuple(identity, max_unrolled_length))
128+
end
129+
@testset "heterogeneous" begin
130+
@testset "stability" begin
131+
tup = (0, 0x0, 0x000)
132+
@test tup === sort(tup)
133+
end
134+
tup = (1, 2, 3, missing, missing)
135+
for t (tup, (1, missing, 2, missing, 3), (missing, missing, 1, 2, 3))
136+
@test tup === @inferred sort(t)
137+
end
138+
end
95139
end
96140

97141
@testset "partialsort" begin

test/typedomainnumbers.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
using
4+
Test,
5+
Base._TypeDomainNumbers.PositiveIntegers,
6+
Base._TypeDomainNumbers.IntegersGreaterThanOne,
7+
Base._TypeDomainNumbers.Constants,
8+
Base._TypeDomainNumberTupleUtils
9+
10+
@testset "type domain numbers" begin
11+
@test n0 isa NonnegativeInteger
12+
@test n1 isa NonnegativeInteger
13+
@test n1 isa PositiveInteger
14+
@testset "succ" begin
15+
for x (n0, n1)
16+
@test x === natural_predecessor(@inferred natural_successor(x))
17+
@test x === natural_predecessor_predecessor(natural_successor(natural_successor(x)))
18+
end
19+
end
20+
@testset "type safety" begin
21+
@test_throws TypeError PositiveInteger{Int}
22+
end
23+
@testset "tuple utils" begin
24+
@test n0 === @inferred tuple_type_domain_length(())
25+
@test n1 === @inferred tuple_type_domain_length((7,))
26+
@test ((), ()) === @inferred split_tuple_into_halves(())
27+
@test ((), (7,)) === @inferred split_tuple_into_halves((7,))
28+
@test ((3,), (7,)) === @inferred split_tuple_into_halves((3, 7))
29+
@test ((3,), (7, 9)) === @inferred split_tuple_into_halves((3, 7, 9))
30+
end
31+
end

0 commit comments

Comments
 (0)