Skip to content

Commit 822a4a0

Browse files
committed
implement generator expressions (#4470)
This introduces the types `Generator`, which maps a function over an iterator, and `IteratorND`, which wraps an iterator with a shape tuple.
1 parent 8f26020 commit 822a4a0

12 files changed

+178
-21
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ CORE_SRCS := $(addprefix $(JULIAHOME)/, \
191191
base/dict.jl \
192192
base/error.jl \
193193
base/essentials.jl \
194+
base/generator.jl \
194195
base/expr.jl \
195196
base/functors.jl \
196197
base/hashing.jl \

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Julia v0.5.0 Release Notes
44
New language features
55
---------------------
66

7+
* Generator expressions, e.g. `f(i) for i in 1:n` (#4470). This returns an iterator
8+
that computes the specified values on demand.
9+
710
* Macro expander functions are now generic, so macros can have multiple definitions
811
(e.g. for different numbers of arguments, or optional arguments) ([#8846], [#9627]).
912
However note that the argument types refer to the syntax tree representation, and not

base/abstractarray.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1226,7 +1226,7 @@ function map!{F}(f::F, dest::AbstractArray, A::AbstractArray)
12261226
return dest
12271227
end
12281228

1229-
function map_to!{T,F}(f::F, offs, st, dest::AbstractArray{T}, A::AbstractArray)
1229+
function map_to!{T,F}(f::F, offs, st, dest::AbstractArray{T}, A)
12301230
# map to dest array, checking the type of each result. if a result does not
12311231
# match, widen the result type and re-dispatch.
12321232
i = offs

base/boot.jl

+1
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ unsafe_convert{T}(::Type{T}, x::T) = x
341341
(::Type{Array{T}}){T}(m::Int, n::Int, o::Int) = Array{T,3}(m, n, o)
342342

343343
# TODO: possibly turn these into deprecations
344+
Array{T,N}(::Type{T}, d::NTuple{N,Int}) = Array{T}(d)
344345
Array{T}(::Type{T}, d::Int...) = Array{T}(d)
345346
Array{T}(::Type{T}, m::Int) = Array{T,1}(m)
346347
Array{T}(::Type{T}, m::Int,n::Int) = Array{T,2}(m,n)

base/coreimg.jl

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ macro doc(str, def) Expr(:escape, def) end
2222

2323
## Load essential files and libraries
2424
include("essentials.jl")
25+
include("generator.jl")
2526
include("reflection.jl")
2627
include("options.jl")
2728

base/generator.jl

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Generator(f, iter)
3+
4+
Given a function `f` and an iterator `iter`, construct an iterator that yields
5+
the values of `f` applied to the elements of `iter`.
6+
The syntax `f(x) for x in iter` is syntax for constructing an instance of this
7+
type.
8+
"""
9+
immutable Generator{I,F}
10+
f::F
11+
iter::I
12+
end
13+
14+
start(g::Generator) = start(g.iter)
15+
done(g::Generator, s) = done(g.iter, s)
16+
function next(g::Generator, s)
17+
v, s2 = next(g.iter, s)
18+
g.f(v), s2
19+
end
20+
21+
collect(g::Generator) = map(g.f, g.iter)

base/iterator.jl

+48
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,51 @@ eltype{I1,I2}(::Type{Prod{I1,I2}}) = tuple_type_cons(eltype(I1), eltype(I2))
292292
x = prod_next(p, st)
293293
((x[1][1],x[1][2]...), x[2])
294294
end
295+
296+
_size(p::Prod2) = (length(p.a), length(p.b))
297+
_size(p::Prod) = (length(p.a), _size(p.b)...)
298+
299+
"""
300+
IteratorND(iter, dims)
301+
302+
Given an iterator `iter` and dimensions tuple `dims`, return an iterator that
303+
yields the same values as `iter`, but with the specified multi-dimensional shape.
304+
For example, this determines the shape of the array returned when `collect` is
305+
applied to this iterator.
306+
"""
307+
immutable IteratorND{I,N}
308+
iter::I
309+
dims::NTuple{N,Int}
310+
311+
function (::Type{IteratorND}){I,N}(iter::I, shape::NTuple{N,Integer})
312+
if length(iter) != prod(shape)
313+
throw(DimensionMismatch("dimensions $shape must be consistent with iterator length $(iter(a))"))
314+
end
315+
new{I,N}(iter, shape)
316+
end
317+
(::Type{IteratorND}){I<:AbstractProdIterator}(p::I) = IteratorND(p, _size(p))
318+
end
319+
320+
start(i::IteratorND) = start(i.iter)
321+
done(i::IteratorND, s) = done(i.iter, s)
322+
next(i::IteratorND, s) = next(i.iter, s)
323+
324+
size(i::IteratorND) = i.dims
325+
length(i::IteratorND) = prod(size(i))
326+
ndims{I,N}(::IteratorND{I,N}) = N
327+
328+
eltype{I}(::IteratorND{I}) = eltype(I)
329+
330+
collect(i::IteratorND) = copy!(Array(eltype(i),size(i)), i)
331+
332+
function collect{I<:IteratorND}(g::Generator{I})
333+
sz = size(g.iter)
334+
if length(g.iter) == 0
335+
return Array(Union{}, sz)
336+
end
337+
st = start(g)
338+
first, st = next(g, st)
339+
dest = Array(typeof(first), sz)
340+
dest[1] = first
341+
return map_to!(g.f, 2, st, dest, g.iter)
342+
end

base/sysimg.jl

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ end
3030
include("essentials.jl")
3131
include("docs/bootstrap.jl")
3232
include("base.jl")
33+
include("generator.jl")
3334
include("reflection.jl")
3435
include("options.jl")
3536

doc/manual/arrays.rst

+31
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,37 @@ that the result is of type ``Float64`` by writing::
213213

214214
Float64[ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
215215

216+
.. _man-generator-expressions:
217+
218+
Generator Expressions
219+
---------------------
220+
221+
Comprehensions can also be written without the enclosing square brackets, producing
222+
an object known as a generator. This object can be iterated to produce values on
223+
demand, instead of allocating an array and storing them in advance
224+
(see :ref:`_man-interfaces-iteration`).
225+
For example, the following expression sums a series without allocating memory::
226+
227+
julia> sum(1/n^2 for n=1:1000)
228+
1.6439345666815615
229+
230+
When writing a generator expression with multiple dimensions, it needs to be
231+
enclosed in parentheses to avoid ambiguity::
232+
233+
julia> collect(1/(i+j) for i=1:2, j=1:2)
234+
ERROR: function collect does not accept keyword arguments
235+
236+
In this call, the range ``j=1:2`` was interpreted as a second argument to
237+
``collect``. This is fixed by adding parentheses::
238+
239+
julia> collect((1/(i+j) for i=1:2, j=1:2))
240+
2x2 Array{Float64,2}:
241+
0.5 0.333333
242+
0.333333 0.25
243+
244+
Note that ``collect`` gathers the values produced by an iterator into an array,
245+
giving the same effect as an array comprehension.
246+
216247
.. _man-array-indexing:
217248

218249
Indexing

src/julia-parser.scm

+33-20
Original file line numberDiff line numberDiff line change
@@ -1401,21 +1401,22 @@
14011401
(parse-comma-separated s parse-eq*))
14021402

14031403
;; as above, but allows both "i=r" and "i in r"
1404+
(define (parse-iteration-spec s)
1405+
(let ((r (parse-eq* s)))
1406+
(cond ((and (pair? r) (eq? (car r) '=)) r)
1407+
((eq? r ':) r)
1408+
((and (length= r 4) (eq? (car r) 'comparison)
1409+
(or (eq? (caddr r) 'in) (eq? (caddr r) '∈)))
1410+
`(= ,(cadr r) ,(cadddr r)))
1411+
(else
1412+
(error "invalid iteration specification")))))
1413+
14041414
(define (parse-comma-separated-iters s)
14051415
(let loop ((ranges '()))
1406-
(let ((r (parse-eq* s)))
1407-
(let ((r (cond ((and (pair? r) (eq? (car r) '=))
1408-
r)
1409-
((eq? r ':)
1410-
r)
1411-
((and (length= r 4) (eq? (car r) 'comparison)
1412-
(or (eq? (caddr r) 'in) (eq? (caddr r) '∈)))
1413-
`(= ,(cadr r) ,(cadddr r)))
1414-
(else
1415-
(error "invalid iteration specification")))))
1416-
(case (peek-token s)
1417-
((#\,) (take-token s) (loop (cons r ranges)))
1418-
(else (reverse! (cons r ranges))))))))
1416+
(let ((r (parse-iteration-spec s)))
1417+
(case (peek-token s)
1418+
((#\,) (take-token s) (loop (cons r ranges)))
1419+
(else (reverse! (cons r ranges)))))))
14191420

14201421
(define (parse-space-separated-exprs s)
14211422
(with-space-sensitive
@@ -1471,6 +1472,12 @@
14711472
(loop (cons nxt lst)))
14721473
((eqv? c #\;) (loop (cons nxt lst)))
14731474
((eqv? c closer) (loop (cons nxt lst)))
1475+
((eq? c 'for)
1476+
(take-token s)
1477+
(let ((gen (parse-generator s nxt #f)))
1478+
(if (eqv? (require-token s) #\,)
1479+
(take-token s))
1480+
(loop (cons gen lst))))
14741481
;; newline character isn't detectable here
14751482
#;((eqv? c #\newline)
14761483
(error "unexpected line break in argument list"))
@@ -1515,7 +1522,7 @@
15151522
(define (parse-comprehension s first closer)
15161523
(let ((r (parse-comma-separated-iters s)))
15171524
(if (not (eqv? (require-token s) closer))
1518-
(error (string "expected " closer))
1525+
(error (string "expected \"" closer "\""))
15191526
(take-token s))
15201527
`(comprehension ,first ,@r)))
15211528

@@ -1525,12 +1532,11 @@
15251532
`(dict_comprehension ,@(cdr c))
15261533
(error "invalid dict comprehension"))))
15271534

1528-
(define (parse-generator s first closer)
1529-
(let ((r (parse-comma-separated-iters s)))
1530-
(if (not (eqv? (require-token s) closer))
1531-
(error (string "expected " closer))
1532-
(take-token s))
1533-
`(macrocall @generator ,first ,@r)))
1535+
(define (parse-generator s first allow-comma)
1536+
(let ((r (if allow-comma
1537+
(parse-comma-separated-iters s)
1538+
(list (parse-iteration-spec s)))))
1539+
`(generator ,first ,@r)))
15341540

15351541
(define (parse-matrix s first closer gotnewline)
15361542
(define (fix head v) (cons head (reverse v)))
@@ -1960,6 +1966,13 @@
19601966
`(tuple ,ex)
19611967
;; value in parentheses (x)
19621968
ex))
1969+
((eq? t 'for)
1970+
(take-token s)
1971+
(let ((gen (parse-generator s ex #t)))
1972+
(if (eqv? (require-token s) #\) )
1973+
(take-token s)
1974+
(error "expected \")\""))
1975+
gen))
19631976
(else
19641977
;; tuple (x,) (x,y) (x...) etc.
19651978
(if (eqv? t #\, )

src/julia-syntax.scm

+17
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,23 @@
19261926
(lower-ccall name RT (cdr argtypes) args))))
19271927
e))
19281928

1929+
'generator
1930+
(lambda (e)
1931+
(let ((expr (cadr e))
1932+
(vars (map cadr (cddr e)))
1933+
(ranges (map caddr (cddr e))))
1934+
(let* ((argname (if (and (length= vars 1) (symbol? (car vars)))
1935+
(car vars)
1936+
(gensy)))
1937+
(splat (if (eq? argname (car vars))
1938+
'()
1939+
`((= (tuple ,@vars) ,argname)))))
1940+
(expand-forms
1941+
`(call (top Generator) (-> ,argname (block ,@splat ,expr))
1942+
,(if (length= ranges 1)
1943+
(car ranges)
1944+
`(call (top IteratorND) (call (top product) ,@ranges))))))))
1945+
19291946
'comprehension
19301947
(lambda (e)
19311948
(expand-forms (lower-comprehension #f (cadr e) (cddr e))))

test/functional.jl

+20
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,23 @@ let
178178
foreach((args...)->push!(a,args), [2,4,6], [10,20,30])
179179
@test a == [(2,10),(4,20),(6,30)]
180180
end
181+
182+
# generators (#4470, #14848)
183+
184+
@test sum(i/2 for i=1:2) == 1.5
185+
@test collect(2i for i=2:5) == [4,6,8,10]
186+
@test collect((i+10j for i=1:2,j=3:4)) == [31 41; 32 42]
187+
@test collect((i+10j for i=1:2,j=3:4,k=1:1)) == reshape([31 41; 32 42], (2,2,1))
188+
189+
let I = Base.IteratorND(1:27,(3,3,3))
190+
@test collect(I) == reshape(1:27,(3,3,3))
191+
@test size(I) == (3,3,3)
192+
@test length(I) == 27
193+
@test eltype(I) === Int
194+
@test ndims(I) == 3
195+
end
196+
197+
let A = collect(Base.Generator(x->2x, Real[1.5,2.5]))
198+
@test A == [3,5]
199+
@test isa(A,Vector{Float64})
200+
end

0 commit comments

Comments
 (0)