From 437f8f6c4b87385f83c03ff5726aa62316efa0c2 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 6 Feb 2019 10:48:16 +0100 Subject: [PATCH 1/5] lazy macro --- README.md | 12 ++++++ REQUIRE | 1 + src/lazybroadcasting.jl | 88 +++++++++++++++++++++++++++++++++++++++-- test/runtests.jl | 16 +++++++- 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e1f9b32..b5903880 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ julia> B = BroadcastArray(+, A, 2); julia> B == A .+ 2 true ``` +Such arrays can also be created using the macro `@lazy` which acts on ordinary +broadcasting expressions, or the macro `@lazydot` which applies `@.` to add dots first: +```julia +julia> C = rand(1000)'; + +julia> D = @lazy exp.(C) + +julia> E = @lazydot 2 + log(C) + +julia> @btime sum(@lazy C .* C'; dims=1) # 1.438 ms (5 allocations: 7.64 MiB) without @lazy + 74.425 μs (7 allocations: 8.08 KiB) +``` ## Multiplication diff --git a/REQUIRE b/REQUIRE index 77bb4351..b5b5d119 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,3 +1,4 @@ julia 0.7 FillArrays 0.3 StaticArrays 0.8.3 +MacroTools \ No newline at end of file diff --git a/src/lazybroadcasting.jl b/src/lazybroadcasting.jl index 967e4b26..2ffe750b 100644 --- a/src/lazybroadcasting.jl +++ b/src/lazybroadcasting.jl @@ -37,9 +37,91 @@ getindex(B::BroadcastArray{<:Any,1}, kr::AbstractVector{<:Integer}) = copy(bc::Broadcasted{<:LazyArrayStyle}) = BroadcastArray(bc) -# issue 16: sum(b, dims=(1,2,3)) faster than sum(b) -Base._sum(b::BroadcastArray{T,N}, ::Colon) where {T,N} = first(Base._sum(b, ntuple(identity, N))) -Base._prod(b::BroadcastArray{T,N}, ::Colon) where {T,N} = first(Base._prod(b, ntuple(identity, N))) +# Replacement for #18. +# Could extend this to other similar reductions in Base... or apply at lower level? +# for (fname, op) in [(:sum, :add_sum), (:prod, :mul_prod), +# (:maximum, :max), (:minimum, :min), +# (:all, :&), (:any, :|)] +function Base._sum(f, A::BroadcastArray, ::Colon) + bc = A.broadcasted + T = Broadcast.combine_eltypes(f ∘ bc.f, bc.args) + out = zero(T) + @simd for I in eachindex(bc) + @inbounds out += f(bc[I]) + end + out +end +function Base._prod(f, A::BroadcastArray, ::Colon) + bc = A.broadcasted + T = Broadcast.combine_eltypes(f ∘ bc.f, bc.args) + out = one(T) + @simd for I in eachindex(bc) + @inbounds out *= f(bc[I]) + end + out +end + +# Macros for lazy broadcasting +# based on @dawbarton https://discourse.julialang.org/t/19641/20 +# and @tkf https://github.com/JuliaLang/julia/issues/19198#issuecomment-457967851 +# and @chethega https://github.com/JuliaLang/julia/pull/30939 + +export @lazy, @lazydot, @□, @⊡ + +lazy(::Any) = error("function `lazy` must be called with a dot") +struct LazyCast{T} + value::T +end +Broadcast.broadcasted(::typeof(lazy), x) = LazyCast(x) +Base.materialize(x::LazyCast) = BroadcastArray(x.value) + +lazyhelp = """ + @lazy A .+ B == @□ A .+ B + @lazydot A + B == @⊡ A + B + +Macros for creating lazy `BroadcastArray`s: `@lazy` expects a broadcasting expression, +while `@lazydot` applies `@.` first. Short forms are typed `@\\square` and `@\\boxdot` +(or perhaps `@z` & `@ż` except that `ż` seems hard to enter at the REPL). +""" + +@doc lazyhelp +macro lazy(ex) + checkex(ex) + :( lazy.($(esc(ex))) ) +end +@doc lazyhelp +macro □(ex) + checkex(ex) + :( lazy.($(esc(ex))) ) +end + +@doc lazyhelp +macro lazydot(ex) + checkex(ex, "@lazydot") + :( @. lazy($(esc(ex))) ) +end +@doc lazyhelp +macro ⊡(ex) + checkex(ex, "@lazydot") + :( @. lazy($(esc(ex))) ) +end + +using MacroTools + +function checkex(ex, name="@lazy") + if @capture(ex, (arg__,) = val_ ) + if arg[2]==:dims + throw(ArgumentError("$name is capturing keyword arguments, try with `; dims = $val` instead of a comma")) + else + throw(ArgumentError("$name is probably capturing capturing keyword arguments, needs a single expression")) + end + end + if @capture(ex, (arg_,rest__) ) + throw(ArgumentError("$name is capturing more than one expression, try $name($arg) with brackets")) + end + ex +end + BroadcastStyle(::Type{<:BroadcastArray{<:Any,N}}) where N = LazyArrayStyle{N}() BroadcastStyle(L::LazyArrayStyle{N}, ::StaticArrayStyle{N}) where N = L diff --git a/test/runtests.jl b/test/runtests.jl index fef65eba..3478727a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -164,14 +164,28 @@ end @testset "BroadcastArray" begin A = randn(6,6) + B = BroadcastArray(exp, A) + B′ = @lazy exp.(A) + B′′ = @lazydot exp(A) @test Matrix(B) == exp.(A) + @test Matrix(B′) == exp.(A) + @test Matrix(B′′) == exp.(A) C = BroadcastArray(+, A, 2) + C′ = @lazy A .+ 2 + C′′ = @lazydot A + 2 @test C == A .+ 2 + @test C′ == A .+ 2 + @test C′′ == A .+ 2 + D = BroadcastArray(+, A, C) + D′ = @lazy A + C + D′′ = @lazydot A + C @test D == A + C - + @test D′ == A + C + @test D′′ == A + C + @test sum(B) ≈ sum(exp, A) @test sum(C) ≈ sum(A .+ 2) From 4e27ad148fa29d8c68c0bb4fe194a3fb8373a68e Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 6 Feb 2019 11:23:32 +0100 Subject: [PATCH 2/5] Broadcast.materialize not Base --- src/lazybroadcasting.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lazybroadcasting.jl b/src/lazybroadcasting.jl index 2ffe750b..e5737e62 100644 --- a/src/lazybroadcasting.jl +++ b/src/lazybroadcasting.jl @@ -61,7 +61,7 @@ function Base._prod(f, A::BroadcastArray, ::Colon) out end -# Macros for lazy broadcasting +# Macros for lazy broadcasting, #21 WIP # based on @dawbarton https://discourse.julialang.org/t/19641/20 # and @tkf https://github.com/JuliaLang/julia/issues/19198#issuecomment-457967851 # and @chethega https://github.com/JuliaLang/julia/pull/30939 @@ -73,7 +73,7 @@ struct LazyCast{T} value::T end Broadcast.broadcasted(::typeof(lazy), x) = LazyCast(x) -Base.materialize(x::LazyCast) = BroadcastArray(x.value) +Broadcast.materialize(x::LazyCast) = BroadcastArray(x.value) lazyhelp = """ @lazy A .+ B == @□ A .+ B From a8b5b9c80bdf3856cc6e590894d9fd1a52fbb2b0 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 6 Feb 2019 11:49:36 +0100 Subject: [PATCH 3/5] spaces --- src/lazybroadcasting.jl | 62 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/lazybroadcasting.jl b/src/lazybroadcasting.jl index e5737e62..d04f89a9 100644 --- a/src/lazybroadcasting.jl +++ b/src/lazybroadcasting.jl @@ -43,22 +43,22 @@ copy(bc::Broadcasted{<:LazyArrayStyle}) = BroadcastArray(bc) # (:maximum, :max), (:minimum, :min), # (:all, :&), (:any, :|)] function Base._sum(f, A::BroadcastArray, ::Colon) - bc = A.broadcasted - T = Broadcast.combine_eltypes(f ∘ bc.f, bc.args) - out = zero(T) - @simd for I in eachindex(bc) - @inbounds out += f(bc[I]) - end - out + bc = A.broadcasted + T = Broadcast.combine_eltypes(f ∘ bc.f, bc.args) + out = zero(T) + @simd for I in eachindex(bc) + @inbounds out += f(bc[I]) + end + out end function Base._prod(f, A::BroadcastArray, ::Colon) - bc = A.broadcasted - T = Broadcast.combine_eltypes(f ∘ bc.f, bc.args) - out = one(T) - @simd for I in eachindex(bc) - @inbounds out *= f(bc[I]) - end - out + bc = A.broadcasted + T = Broadcast.combine_eltypes(f ∘ bc.f, bc.args) + out = one(T) + @simd for I in eachindex(bc) + @inbounds out *= f(bc[I]) + end + out end # Macros for lazy broadcasting, #21 WIP @@ -76,8 +76,8 @@ Broadcast.broadcasted(::typeof(lazy), x) = LazyCast(x) Broadcast.materialize(x::LazyCast) = BroadcastArray(x.value) lazyhelp = """ - @lazy A .+ B == @□ A .+ B - @lazydot A + B == @⊡ A + B + @lazy A .+ B == @□ A .+ B + @lazydot A + B == @⊡ A + B Macros for creating lazy `BroadcastArray`s: `@lazy` expects a broadcasting expression, while `@lazydot` applies `@.` first. Short forms are typed `@\\square` and `@\\boxdot` @@ -86,40 +86,40 @@ while `@lazydot` applies `@.` first. Short forms are typed `@\\square` and `@\\b @doc lazyhelp macro lazy(ex) - checkex(ex) + checkex(ex) :( lazy.($(esc(ex))) ) end @doc lazyhelp macro □(ex) - checkex(ex) + checkex(ex) :( lazy.($(esc(ex))) ) end @doc lazyhelp macro lazydot(ex) - checkex(ex, "@lazydot") + checkex(ex, "@lazydot") :( @. lazy($(esc(ex))) ) end @doc lazyhelp macro ⊡(ex) - checkex(ex, "@lazydot") + checkex(ex, "@lazydot") :( @. lazy($(esc(ex))) ) end using MacroTools function checkex(ex, name="@lazy") - if @capture(ex, (arg__,) = val_ ) - if arg[2]==:dims - throw(ArgumentError("$name is capturing keyword arguments, try with `; dims = $val` instead of a comma")) - else - throw(ArgumentError("$name is probably capturing capturing keyword arguments, needs a single expression")) - end - end - if @capture(ex, (arg_,rest__) ) - throw(ArgumentError("$name is capturing more than one expression, try $name($arg) with brackets")) - end - ex + if @capture(ex, (arg__,) = val_ ) + if arg[2]==:dims + throw(ArgumentError("$name is capturing keyword arguments, try with `; dims = $val` instead of a comma")) + else + throw(ArgumentError("$name is probably capturing capturing keyword arguments, needs a single expression")) + end + end + if @capture(ex, (arg_,rest__) ) + throw(ArgumentError("$name is capturing more than one expression, try $name($arg) with brackets")) + end + ex end From e3eebd75d272e7232a472b0b903d7e4e951ee67c Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 6 Feb 2019 21:30:23 +0100 Subject: [PATCH 4/5] lazy tilde --- src/lazybroadcasting.jl | 49 ++++++++++++++--------------------------- test/runtests.jl | 12 +++++----- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/lazybroadcasting.jl b/src/lazybroadcasting.jl index d04f89a9..458681db 100644 --- a/src/lazybroadcasting.jl +++ b/src/lazybroadcasting.jl @@ -66,58 +66,43 @@ end # and @tkf https://github.com/JuliaLang/julia/issues/19198#issuecomment-457967851 # and @chethega https://github.com/JuliaLang/julia/pull/30939 -export @lazy, @lazydot, @□, @⊡ +export @~ -lazy(::Any) = error("function `lazy` must be called with a dot") +lazy(::Any) = throw(ArgumentError("function `lazy` exists only for its effect on broadcasting, see the macro @~")) struct LazyCast{T} value::T end Broadcast.broadcasted(::typeof(lazy), x) = LazyCast(x) Broadcast.materialize(x::LazyCast) = BroadcastArray(x.value) -lazyhelp = """ - @lazy A .+ B == @□ A .+ B - @lazydot A + B == @⊡ A + B - -Macros for creating lazy `BroadcastArray`s: `@lazy` expects a broadcasting expression, -while `@lazydot` applies `@.` first. Short forms are typed `@\\square` and `@\\boxdot` -(or perhaps `@z` & `@ż` except that `ż` seems hard to enter at the REPL). """ + @~ expr -@doc lazyhelp -macro lazy(ex) - checkex(ex) - :( lazy.($(esc(ex))) ) -end -@doc lazyhelp -macro □(ex) - checkex(ex) - :( lazy.($(esc(ex))) ) -end +Macro for creating lazy `BroadcastArray`s. +Expects a broadcasting expression, possibly created by the `@.` macro: +``` +julia> @~ A .+ B ./ 2 -@doc lazyhelp -macro lazydot(ex) - checkex(ex, "@lazydot") - :( @. lazy($(esc(ex))) ) -end -@doc lazyhelp -macro ⊡(ex) - checkex(ex, "@lazydot") - :( @. lazy($(esc(ex))) ) +julia> @~ @. A + B / 2 +``` +""" +macro ~(ex) + checkex(ex) + esc( :( $lazy.($ex) ) ) end using MacroTools -function checkex(ex, name="@lazy") +function checkex(ex) if @capture(ex, (arg__,) = val_ ) if arg[2]==:dims - throw(ArgumentError("$name is capturing keyword arguments, try with `; dims = $val` instead of a comma")) + throw(ArgumentError("@~ is capturing keyword arguments, try with `; dims = $val` instead of a comma")) else - throw(ArgumentError("$name is probably capturing capturing keyword arguments, needs a single expression")) + throw(ArgumentError("@~ is probably capturing capturing keyword arguments, try with ; or brackets")) end end if @capture(ex, (arg_,rest__) ) - throw(ArgumentError("$name is capturing more than one expression, try $name($arg) with brackets")) + throw(ArgumentError("@~ is capturing more than one expression, try $name($arg) with brackets")) end ex end diff --git a/test/runtests.jl b/test/runtests.jl index 3478727a..0a4254a2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -166,22 +166,22 @@ end A = randn(6,6) B = BroadcastArray(exp, A) - B′ = @lazy exp.(A) - B′′ = @lazydot exp(A) + B′ = @~ exp.(A) + B′′ = @~ @. exp(A) @test Matrix(B) == exp.(A) @test Matrix(B′) == exp.(A) @test Matrix(B′′) == exp.(A) C = BroadcastArray(+, A, 2) - C′ = @lazy A .+ 2 - C′′ = @lazydot A + 2 + C′ = @~ A .+ 2 + C′′ = @~ @. A + 2 @test C == A .+ 2 @test C′ == A .+ 2 @test C′′ == A .+ 2 D = BroadcastArray(+, A, C) - D′ = @lazy A + C - D′′ = @lazydot A + C + D′ = @~ A + C + D′′ = @~ @. A + C @test D == A + C @test D′ == A + C @test D′′ == A + C From c5073428ef9c94c4bbd8fe7fa739f07f99d290b9 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 6 Feb 2019 21:43:45 +0100 Subject: [PATCH 5/5] readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b5903880..c3cf1756 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,16 @@ julia> B = BroadcastArray(+, A, 2); julia> B == A .+ 2 true ``` -Such arrays can also be created using the macro `@lazy` which acts on ordinary -broadcasting expressions, or the macro `@lazydot` which applies `@.` to add dots first: +Such arrays can also be created using the macro `@~` which acts on ordinary +broadcasting expressions: ```julia julia> C = rand(1000)'; -julia> D = @lazy exp.(C) +julia> D = @~ exp.(C) -julia> E = @lazydot 2 + log(C) +julia> E = @~ @. 2 + log(C) -julia> @btime sum(@lazy C .* C'; dims=1) # 1.438 ms (5 allocations: 7.64 MiB) without @lazy +julia> @btime sum(@~ C .* C'; dims=1) # without `@~`, 1.438 ms (5 allocations: 7.64 MiB) 74.425 μs (7 allocations: 8.08 KiB) ```