Skip to content

Commit 6edf6d9

Browse files
authored
allow slurping in lhs of assignment (#37410)
fixes #2626
1 parent ba39b88 commit 6edf6d9

File tree

8 files changed

+174
-21
lines changed

8 files changed

+174
-21
lines changed

NEWS.md

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ New language features
2828
* The postfix conjugate transpose operator `'` now accepts Unicode modifiers as
2929
suffixes, so e.g. `a'ᵀ` is parsed as `var"'ᵀ"(a)`, which can be defined by the
3030
user. `a'ᵀ` parsed as `a' * ᵀ` before, so this is a minor change ([#37247]).
31+
* It is now possible to use varargs on the left-hand side of assignments for taking any
32+
number of items from the front of an iterable collection, while also collecting the rest,
33+
like `a, b... = [1, 2, 3]`, for example. This syntax is implemented using `Base.rest`,
34+
which can be overloaded to customize its behavior for different collection types
35+
([#37410]).
3136

3237
Language changes
3338
----------------
@@ -96,6 +101,8 @@ New library functions
96101
efficiently ([#35816]).
97102
* New function `addenv` for adding environment mappings into a `Cmd` object, returning the new `Cmd` object.
98103
* New function `insorted` for determining whether an element is in a sorted collection or not ([#37490]).
104+
* New function `Base.rest` for taking the rest of a collection, starting from a specific
105+
iteration state, in a generic way ([#37410]).
99106

100107
New library features
101108
--------------------

base/abstractarray.jl

+8
Original file line numberDiff line numberDiff line change
@@ -2399,3 +2399,11 @@ function hash(A::AbstractArray, h::UInt)
23992399

24002400
return h
24012401
end
2402+
2403+
# The semantics of `collect` are weird. Better to write our own
2404+
function rest(a::AbstractArray{T}, state...) where {T}
2405+
v = Vector{T}(undef, 0)
2406+
# assume only very few items are taken from the front
2407+
sizehint!(v, length(a))
2408+
return foldl(push!, Iterators.rest(a, state...), init=v)
2409+
end

base/tuple.jl

+32
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,38 @@ function indexed_iterate(I, i, state)
9696
x
9797
end
9898

99+
"""
100+
Base.rest(collection[, itr_state])
101+
102+
Generic function for taking the tail of `collection`, starting from a specific iteration
103+
state `itr_state`. Return a `Tuple`, if `collection` itself is a `Tuple`, a `Vector`, if
104+
`collection` is an `AbstractArray` and `Iterators.rest(collection[, itr_state])` otherwise.
105+
Can be overloaded for user-defined collection types to customize the behavior of slurping
106+
in assignments, like `a, b... = collection`.
107+
108+
!!! compat "Julia 1.6"
109+
`Base.rest` requires at least Julia 1.6.
110+
111+
# Examples
112+
```jldoctest
113+
julia> a = [1 2; 3 4]
114+
2×2 Matrix{Int64}:
115+
1 2
116+
3 4
117+
118+
julia> first, state = iterate(a)
119+
(1, 2)
120+
121+
julia> first, Base.rest(a, state)
122+
(1, [3, 2, 4])
123+
```
124+
"""
125+
function rest end
126+
rest(t::Tuple) = t
127+
rest(t::Tuple, i::Int) = ntuple(x -> getfield(t, x+i-1), length(t)-i+1)
128+
rest(a::Array, i::Int=1) = a[i:end]
129+
rest(itr, state...) = Iterators.rest(itr, state...)
130+
99131
# Use dispatch to avoid a branch in first
100132
first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty"))
101133
first(t::Tuple) = t[1]

doc/src/base/collections.md

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Base.filter!
138138
Base.replace(::Any, ::Pair...)
139139
Base.replace(::Base.Callable, ::Any)
140140
Base.replace!
141+
Base.rest
141142
```
142143

143144
## Indexable Collections

src/julia-syntax.scm

+50-21
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,8 @@
14301430
,@(reverse after)
14311431
(unnecessary (tuple ,@(reverse elts))))
14321432
(let ((L (car lhss))
1433-
(R (car rhss)))
1433+
;; rhss can be null iff L is a vararg
1434+
(R (if (null? rhss) '() (car rhss))))
14341435
(cond ((and (symbol-like? L)
14351436
(or (not (pair? R)) (quoted? R) (equal? R '(null)))
14361437
;; overwrite var immediately if it doesn't occur elsewhere
@@ -1442,6 +1443,16 @@
14421443
(cons (make-assignment L R) stmts)
14431444
after
14441445
(cons R elts)))
1446+
((vararg? L)
1447+
(if (null? (cdr lhss))
1448+
(let ((temp (make-ssavalue)))
1449+
`(block ,@(reverse stmts)
1450+
(= ,temp (tuple ,@rhss))
1451+
,@(reverse after)
1452+
(= ,(cadr L) ,temp)
1453+
(unnecessary (tuple ,@(reverse elts) (... ,temp)))))
1454+
(error (string "invalid \"...\" on non-final assignment location \""
1455+
(cadr L) "\""))))
14451456
((vararg? R)
14461457
(let ((temp (make-ssavalue)))
14471458
`(block ,@(reverse stmts)
@@ -2066,6 +2077,7 @@
20662077
(define (sides-match? l r)
20672078
;; l and r either have equal lengths, or r has a trailing ...
20682079
(cond ((null? l) (null? r))
2080+
((vararg? (car l)) #t)
20692081
((null? r) #f)
20702082
((vararg? (car r)) (null? (cdr r)))
20712083
(else (sides-match? (cdr l) (cdr r)))))
@@ -2075,26 +2087,43 @@
20752087
(expand-forms
20762088
(tuple-to-assignments lhss x))
20772089
;; (a, b, ...) = other
2078-
(let* ((xx (if (or (and (symbol? x) (not (memq x lhss)))
2079-
(ssavalue? x))
2080-
x (make-ssavalue)))
2081-
(ini (if (eq? x xx) '() (list (sink-assignment xx (expand-forms x)))))
2082-
(n (length lhss))
2083-
(st (gensy)))
2084-
`(block
2085-
(local ,st)
2086-
,@ini
2087-
,.(map (lambda (i lhs)
2088-
(expand-forms
2089-
(lower-tuple-assignment
2090-
(if (= i (- n 1))
2091-
(list lhs)
2092-
(list lhs st))
2093-
`(call (top indexed_iterate)
2094-
,xx ,(+ i 1) ,.(if (eq? i 0) '() `(,st))))))
2095-
(iota n)
2096-
lhss)
2097-
(unnecessary ,xx))))))
2090+
(begin
2091+
;; like memq, but if last element of lhss is (... sym),
2092+
;; check against sym instead
2093+
(define (in-lhs? x lhss)
2094+
(if (null? lhss)
2095+
#f
2096+
(let ((l (car lhss)))
2097+
(cond ((and (pair? l) (eq? (car l) '|...|))
2098+
(if (null? (cdr lhss))
2099+
(eq? (cadr l) x)
2100+
(error (string "invalid \"...\" on non-final assignment location \""
2101+
(cadr l) "\""))))
2102+
((eq? l x) #t)
2103+
(else (in-lhs? x (cdr lhss)))))))
2104+
;; in-lhs? also checks for invalid syntax, so always call it first
2105+
(let* ((xx (if (or (and (not (in-lhs? x lhss)) (symbol? x))
2106+
(ssavalue? x))
2107+
x (make-ssavalue)))
2108+
(ini (if (eq? x xx) '() (list (sink-assignment xx (expand-forms x)))))
2109+
(n (length lhss))
2110+
(st (gensy)))
2111+
`(block
2112+
(local ,st)
2113+
,@ini
2114+
,.(map (lambda (i lhs)
2115+
(expand-forms
2116+
(if (and (pair? lhs) (eq? (car lhs) '|...|))
2117+
`(= ,(cadr lhs) (call (top rest) ,xx ,.(if (eq? i 0) '() `(,st))))
2118+
(lower-tuple-assignment
2119+
(if (= i (- n 1))
2120+
(list lhs)
2121+
(list lhs st))
2122+
`(call (top indexed_iterate)
2123+
,xx ,(+ i 1) ,.(if (eq? i 0) '() `(,st)))))))
2124+
(iota n)
2125+
lhss)
2126+
(unnecessary ,xx)))))))
20982127
((typed_hcat)
20992128
(error "invalid spacing in left side of indexed assignment"))
21002129
((typed_vcat)

test/abstractarray.jl

+7
Original file line numberDiff line numberDiff line change
@@ -1193,3 +1193,10 @@ end
11931193
@test last(itr, 1) == [itr[end]]
11941194
@test_throws ArgumentError last(itr, -6)
11951195
end
1196+
1197+
@testset "Base.rest" begin
1198+
a = reshape(1:4, 2, 2)'
1199+
@test Base.rest(a) == a[:]
1200+
_, st = iterate(a)
1201+
@test Base.rest(a, st) == [3, 2, 4]
1202+
end

test/syntax.jl

+51
Original file line numberDiff line numberDiff line change
@@ -2532,3 +2532,54 @@ end
25322532

25332533
# PR #37973
25342534
@test Meta.parse("1¦2⌿3") == Expr(:call, , 1, Expr(:call, :⌿, 2, 3))
2535+
2536+
@testset "slurp in assignments" begin
2537+
res = begin x, y, z... = 1:7 end
2538+
@test res == 1:7
2539+
@test x == 1 && y == 2
2540+
@test z == Vector(3:7)
2541+
2542+
res = begin x, y, z... = [1, 2] end
2543+
@test res == [1, 2]
2544+
@test x == 1 && y == 2
2545+
@test z == Int[]
2546+
2547+
x = 1
2548+
res = begin x..., = x end
2549+
@test res == 1
2550+
@test x == 1
2551+
2552+
x, y, z... = 1:7
2553+
res = begin y, z, x... = z..., x, y end
2554+
@test res == ((3:7)..., 1, 2)
2555+
@test y == 3
2556+
@test z == 4
2557+
@test x == ((5:7)..., 1, 2)
2558+
2559+
res = begin x, _, y... = 1, 2 end
2560+
@test res == (1, 2)
2561+
@test x == 1
2562+
@test y == ()
2563+
2564+
res = begin x, y... = 1 end
2565+
@test res == 1
2566+
@test x == 1
2567+
@test y == Iterators.rest(1, nothing)
2568+
2569+
res = begin x, y, z... = 1, 2, 3:5 end
2570+
@test res == (1, 2, 3:5)
2571+
@test x == 1 && y == 2
2572+
@test z == (3:5,)
2573+
2574+
@test Meta.isexpr(Meta.@lower(begin a, b..., c = 1:3 end), :error)
2575+
@test Meta.isexpr(Meta.@lower(begin a, b..., c = 1, 2, 3 end), :error)
2576+
@test Meta.isexpr(Meta.@lower(begin a, b..., c... = 1, 2, 3 end), :error)
2577+
2578+
@test_throws BoundsError begin x, y, z... = 1:1 end
2579+
@test_throws BoundsError begin x, y, _, z... = 1, 2 end
2580+
2581+
car((a, d...)) = a
2582+
cdr((a, d...)) = d
2583+
@test car(1:3) == 1
2584+
@test cdr(1:3) == [2, 3]
2585+
end

test/tuple.jl

+18
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,21 @@ end
575575
@test_throws BoundsError (1,2.0)[0:1]
576576
@test_throws BoundsError (1,2.0)[0:0]
577577
end
578+
579+
@testset "Base.rest" begin
580+
t = (1, 2.0, 0x03, 4f0)
581+
@test Base.rest(t) === t
582+
@test Base.rest(t, 2) === (2.0, 0x03, 4f0)
583+
584+
a = [1 2; 3 4]
585+
@test Base.rest(a) == a[:]
586+
@test pointer(Base.rest(a)) != pointer(a)
587+
@test Base.rest(a, 3) == [2, 4]
588+
589+
itr = (-i for i in a)
590+
@test Base.rest(itr) == itr
591+
_, st = iterate(itr)
592+
r = Base.rest(itr, st)
593+
@test r isa Iterators.Rest
594+
@test collect(r) == -[3, 2, 4]
595+
end

0 commit comments

Comments
 (0)