Skip to content

Commit 8d3fe04

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 683da41 commit 8d3fe04

File tree

5 files changed

+289
-0
lines changed

5 files changed

+289
-0
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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,91 @@ 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._TypeDomainNumbers.Utils, Base._TypeDomainNumberTupleUtils, Base._TupleTypeByLength
1743+
using Base: tail
1744+
using Base.Order: Ordering, lt
1745+
export sort_tuple_stable
1746+
function merge_recursive((@nospecialize ord::Ordering), a::Tuple, b::Tuple)
1747+
if a isa Tuple1OrMore
1748+
a
1749+
else
1750+
b
1751+
end
1752+
end
1753+
function merge_recursive(ord::Ordering, a::Tuple1OrMore, b::Tuple1OrMore)
1754+
l = first(a)
1755+
r = first(b)
1756+
x = tail(a)
1757+
y = tail(b)
1758+
if lt(ord, r, l)
1759+
let rec = merge_recursive(ord, a, y)
1760+
(r, rec...)
1761+
end
1762+
else
1763+
let rec = merge_recursive(ord, x, b)
1764+
(l, rec...)
1765+
end
1766+
end
1767+
end
1768+
function merge_nontrivial(ord::Ordering, a::Tuple1OrMore, b::Tuple1OrMore)
1769+
merge_recursive(ord, a, b)
1770+
end
1771+
function split_tuple(@nospecialize tup::Tuple2OrMore)
1772+
len = tuple_type_domain_length(tup)
1773+
len_l = half_floor_nontrivial(len)
1774+
len_r = half_ceiling_nontrivial(len)
1775+
tup_l = skip_from_tail_nontrivial(tup, len_r)
1776+
tup_r = skip_from_front_nontrivial(tup, len_l)
1777+
(tup_l, tup_r)
1778+
end
1779+
function sort_recursive((@nospecialize ord::Ordering), @nospecialize tup::Tuple{Any})
1780+
tup
1781+
end
1782+
function sort_recursive(ord::Ordering, tup::Tuple2OrMore)
1783+
(tup_l, tup_r) = split_tuple(tup)
1784+
sorted_l = sort_recursive(ord, tup_l)
1785+
sorted_r = sort_recursive(ord, tup_r)
1786+
merge_nontrivial(ord, sorted_l, sorted_r)
1787+
end
1788+
function sort_tuple_stable_2_or_more(ord::Ordering, tup::Tuple2OrMore)
1789+
sort_recursive(ord, tup)
1790+
end
1791+
function sort_tuple_array_fallback(ord::Ordering, tup::Tuple2OrMore)
1792+
vec = if tup isa NTuple
1793+
[tup...]
1794+
else
1795+
Any[tup...]
1796+
end
1797+
sort!(vec; order = ord)
1798+
(vec...,)
1799+
end
1800+
function sort_tuple_stable((@nospecialize ord::Ordering), @nospecialize tup::Tuple)
1801+
if tup isa Tuple2OrMore
1802+
if tup isa Tuple32OrMore
1803+
sort_tuple_array_fallback(ord, tup)
1804+
else
1805+
sort_tuple_stable_2_or_more(ord, tup)
1806+
end
1807+
else
1808+
tup
1809+
end
1810+
end
1811+
end
1812+
1813+
function sort(
1814+
tup::Tuple;
1815+
lt = isless,
1816+
by = identity,
1817+
rev::Union{Nothing, Bool} = nothing,
1818+
order::Ordering = Forward,
1819+
)
1820+
o = ord(lt, by, rev, order)
1821+
_SortTupleStable.sort_tuple_stable(o, tup)
1822+
end
1823+
17391824
## partialsortperm: the permutation to sort the first k elements of an array ##
17401825

17411826
"""

base/typedomainnumbers.jl

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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, half_floor_nontrivial, half_ceiling_nontrivial
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+
function half_floor_nontrivial(@nospecialize m::IntegerGreaterThanOne)
97+
half_floor(m)
98+
end
99+
function half_ceiling_nontrivial(@nospecialize m::IntegerGreaterThanOne)
100+
half_ceiling(m)
101+
end
102+
end
103+
end
104+
105+
module _TupleTypeByLength
106+
export
107+
Tuple1OrMore, Tuple2OrMore, Tuple3OrMore, Tuple4OrMore, Tuple32OrMore
108+
const Tuple1OrMore = Tuple{Any, Vararg}
109+
const Tuple2OrMore = Tuple{Any, Any, Vararg}
110+
const Tuple3OrMore = Tuple{Any, Any, Any, Vararg}
111+
const Tuple4OrMore = Tuple{Any, Any, Any, Any, Vararg}
112+
const Tuple32OrMore = Base.Any32
113+
end
114+
115+
module _TypeDomainNumberTupleUtils
116+
using
117+
.._TypeDomainNumbers.PositiveIntegers, .._TypeDomainNumbers.IntegersGreaterThanOne,
118+
.._TypeDomainNumbers.Constants, .._TupleTypeByLength
119+
using Base: @assume_effects, front, tail
120+
export
121+
tuple_type_domain_length,
122+
skip_from_front, skip_from_tail,
123+
skip_from_front_nontrivial, skip_from_tail_nontrivial
124+
@assume_effects :foldable :nothrow function tuple_type_domain_length(@nospecialize tup::Tuple)
125+
if tup isa Tuple1OrMore
126+
let t = tail(tup), rec = tuple_type_domain_length(t)
127+
natural_successor(rec)
128+
end
129+
else
130+
n0
131+
end
132+
end
133+
@assume_effects :foldable function skip_from_front((@nospecialize tup::Tuple), @nospecialize skip_count::NonnegativeInteger)
134+
if skip_count isa PositiveIntegerUpperBound
135+
let cm1 = natural_predecessor(skip_count), t = tail(tup)
136+
@inline skip_from_front(t, cm1)
137+
end
138+
else
139+
tup
140+
end
141+
end
142+
@assume_effects :foldable function skip_from_tail((@nospecialize tup::Tuple), @nospecialize skip_count::NonnegativeInteger)
143+
if skip_count isa PositiveIntegerUpperBound
144+
let cm1 = natural_predecessor(skip_count), t = front(tup)
145+
@inline skip_from_tail(t, cm1)
146+
end
147+
else
148+
tup
149+
end
150+
end
151+
function skip_from_front_nontrivial((@nospecialize tup::Tuple2OrMore), @nospecialize skip_count::PositiveInteger)
152+
skip_from_front(tup, skip_count)
153+
end
154+
function skip_from_tail_nontrivial((@nospecialize tup::Tuple2OrMore), @nospecialize skip_count::PositiveInteger)
155+
skip_from_tail(tup, skip_count)
156+
end
157+
end

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

0 commit comments

Comments
 (0)