-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lazy broadcasting macro #21
Conversation
Great! Though I think we should think through the names of the macros carefully. Once
|
Yes, this is just a first attempt, I was playing with macros and found myself in danger of re-inventing the wheel... better to use BroadcastArray. Agree the name is confusing, or too limited, in the context of the rest of this package. One could conceivably make Not exported for now, but this is implemented as |
Codecov Report
@@ Coverage Diff @@
## master #21 +/- ##
=========================================
- Coverage 50.06% 49.56% -0.5%
=========================================
Files 11 11
Lines 785 809 +24
=========================================
+ Hits 393 401 +8
- Misses 392 408 +16
Continue to review full report at Codecov.
|
How about using a supre short macro like
Any reason why for exposing both the expression trees ( |
src/lazybroadcasting.jl
Outdated
@doc lazyhelp | ||
macro lazy(ex) | ||
checkex(ex) | ||
:( lazy.($(esc(ex))) ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't esc(:( $lazy.($ex) ))
better? It'd work better with macros inside @lazy
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never thought to use $
for that, am new to macros. Both give @macroexpand @~ @. A + 2
--> :((LazyArrays.lazy).((+).(A, 2)))
, how do I see the difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry, never mind. I think I was mixing it with something else.
Another point: why do you need |
Yes it could be shorter, I wondered about having a pair of symbols with & without a dot, but don't much like the ones I could think of above. I like the |
I like it! One could imagine |
I should have mentioned that the downside of |
Well spotted, |
Let’s avoid inconsistency if we can. I think a Unicode symbol is better than overloading bitwise not. |
An ascii symbol that is available and not an operator is |
I've never written Anyway no rush, this is mostly to see if it functions well first. My tests like #16 are about large arrays, but perhaps other things (such as allocations of small broadcasts) may need more scrutiny. |
|
Ideally it would look "nice" as dot syntax. Not that it's going to be incorporated into the parser anytime soon, but just better to have it evoke it. y ..= exp..(A) but I think it's more appropriate for a "tiered" broadcast ( □ is OK: y □= exp□(A) but not great. ¬ is also OK: y ¬= exp¬(A) but knowing AppleScript I just see "new line". _, |, and : are not good. |
I (somewhat) agree about this point. But I note that |
Ah, we shouldn't use |
Answering to myself: |
Why not just |
That makes sense too. I also actually thought about |
I think we should keep things minimal for now and avoid adding names if possible. I think |
OK. Avoid introducing new names makes sense. |
I'm quite keen on writing Having to write I presume it's not just me, and that not creating a big array you are about to reduce (over some dimensions) is a big reason you'd want broadcasting. |
I believe so. Actually, using Efficient |
OK, so something like adding a method For the complete reduction, chethega's |
By "need inference", what I meant was "need to call inference API", i.e., this: LazyArrays.jl/src/lazybroadcasting.jl Line 17 in 8f2e313
It looks like |
I found a horrible hack to define a macro julia> ex = :(macro _(ex); :(lazy.($(esc(ex)))); end);
julia> ex.args[1].args[1] = :(::)
:(::)
julia> eval(ex)
@:: (macro with 1 method)
julia> @macroexpand @:: a .+ b
:(Main.lazy.(a .+ b)) |
See also JuliaLang/julia#30485. But indeed it's not recommended to expose information obtained through inference to the user (e.g. via the element type of the array). Since the element type of What happens in practice if you don't define the |
OK, we have the following different cases:
So there's a case for macros for 3,5, and 6. What about 2 and 4? |
I like the list. The present macro Just to have toy examples somewhere, this is the behaviour with some unions. I don't know how objectionable these are, julia> ints = [2,3,4];
julia> maybe = Union{Int,Missing}[5,6,7];
julia> f_un(x) = rand() < 0.01 ? missing : x;
julia> ints .+ maybe' # 3×3 Array{Int64,2}
julia> mat = @~ ints .+ maybe' # 3×3 BroadcastArray{Union{Missing, Int64}, ...
julia> collect(@~ int .+ maybe') # 3×3 Array{Union{Missing, Int64},2}; same for copy(), Array()
julia> sum(int .+ maybe'; dims=1) # 1×3 Array{Int64,2}
julia> sum(@~ int .+ maybe'; dims=1) # 1×3 Array{Union{Missing, Int64},2}
julia> f_un.(ints) # 3-element Array{Int64,1}, usually!
julia> @~ f_un.(ints) # 3-element BroadcastArray{Union{Missing, Int64} I think we should make Setting |
ps. That hack for julia> ex.args[1].args[1] = :(...)
:...
julia> @macroexpand @... a .+ b
:((Main.lazy).(a .+ b)) |
In the PR #17, for materialize(applied(f, applied(g, x, y)) === f(g(x,y)) But there can be other materialize(Mul(A,B)) === materialize(applied(*, A, B)) ≠ A*B Another example where this is useful would be matrix-exponentials times vectors. That is, materialize(applied(*, applied(exp, A), v)) could use the fact that |
@nalimilan I think the performance would be bad. For example, |
@dlfivefifty I think it's better to have just one macro that composes well. That is to say, I suggest to implement x = broadcasted(*, A, B)
y = applied(*, x, C)
broadcasted(+, y, D) Note that this recursive nature is compatible with how With this approach, we cover all the list by composing
Note: if you want to reduce the length by 1, you can also write I know that 3 and 6 are rather long. However:
I think we should avoid using macros whenever possible. However, we better define a really good one if we need to do so. The recursive nature of what I suggest for |
FYI I started implementing |
This plan sounds great, if just a little more ambitious than my PR! Is the idea that |
Yes, my idea for |
There is a case for I'll try to finish the |
Right. But maybe that just means it shouldn't support |
I’m not sure I understand, |
Not AFAICT: julia> similar(Base.Broadcast.broadcasted(+, [1]))
ERROR: MethodError: no method matching similar(::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1},Nothing,typeof(+),Tuple{Array{Int64,1}}}) You need to provide the element type: julia> similar(Base.Broadcast.broadcasted(+, [1]), Int)
1-element Array{Int64,1}:
0 |
Right, but one could easily define a
|
julia> bc = Broadcast.broadcasted(x -> x > 0 ? (x > 1 ? (x > 2 ? nothing : 1) : 1.0) : "a", [1]);
julia> Broadcast.combine_eltypes(bc.f, bc.args)
Any |
Talking to myself: This actually is a type piracy. So maybe it's better to have a factory function |
How does this differ from
If I also wonder if there is room for confusion between these levels. If |
It's nice to be able to write
I think it would go in to
Since it is syntactically clear that |
I really don't like abstract type LazyArray{T,N} <: AbstractArray{T,N} end
struct BroadcastArray{T,N} <: LazyArray{T,N} end
struct ApplyArray{T,N} <: LazyArray{T,N} end then support |
This makes sense. |
Here is a sample implementation of what I proposed in #21 (comment) which can do something like julia> @macroexpand @~ a .+ f(b, c)
:((Base.Broadcast.instantiate)((lazy).(a .+ (LazyArrays.applied)(f, b, c))))
julia> @macroexpand @~ a .+ f(b, c .+ d)
:((Base.Broadcast.instantiate)((lazy).(a .+ (LazyArrays.applied)(f, b, (Base.Broadcast.instantiate)((lazy).(c .+ d)))))) Code: using LazyArrays
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) = x.value
is_call(ex::Expr) =
ex.head == :call && !startswith(String(ex.args[1]), ".")
is_dotcall(ex::Expr) =
ex.head == :. || (ex.head == :call && startswith(String(ex.args[1]), "."))
# e.g., `f.(x, y, z)` or `x .+ y .+ z`
lazy_expr(x) = x
function lazy_expr(ex::Expr)
if is_dotcall(ex)
return bc_expr(ex)
elseif is_call(ex)
return app_expr(ex)
else
# TODO: Maybe better to support `a ? b : c` etc.? But how?
return ex
end
end
function bc_expr(ex::Expr)
@assert is_dotcall(ex)
return :($(Broadcast.instantiate)($lazy.($(bc_expr_impl(ex)))))
end
bc_expr_impl(x) = x
function bc_expr_impl(ex::Expr)
# walk down chain of dot calls
if is_dotcall(ex)
return Expr(ex.head,
lazy_expr(ex.args[1]), # function name (`f`, `.+`, etc.)
bc_expr_impl.(ex.args[2:end])...) # arguments
else
return lazy_expr(ex)
end
end
function app_expr(ex::Expr)
@assert is_call(ex)
# instantiate?
return app_expr_impl(ex)
end
app_expr_impl(x) = x
function app_expr_impl(ex::Expr)
# walk down chain of calls and lazy-ify them
if is_call(ex)
return :($applied($(app_expr_impl.(ex.args)...)))
else
return lazy_expr(ex)
end
end
macro ~(ex)
# checkex(ex)
esc(lazy_expr(ex))
end |
Let me know when this is ready to merge. |
This PR adds a macro
@lazy
to turn broadcasting expressions into BroadcastArrays, and@lazydot
which applies@.
first. (It also replaces #18 with an explicit sum method.)The clever idea here to hack broadcasting is due to @dawbarton and @tkf, discussed here:
https://discourse.julialang.org/t/boolean-short-circuit-fusion/19641/20
JuliaLang/julia#19198 (comment)
JuliaLang/julia#30939
Perhaps this package would prefer an
@lazy
which also convertedvcat -> Vcat
etc, haven't thought about that. The name@lazy
also clashes withMacroToolsLazy.jl; I was going to suggest@z
&@ż
as short names but can't seem to typeż
at the REPL (on a mac)... how ugly are@□
&@⊡
?