-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
RFC: Use @: to construct a broadcasted object #31088
base: master
Are you sure you want to change the base?
Conversation
@@ -11,7 +11,7 @@ using .Base.Cartesian | |||
using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin, | |||
_msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias | |||
import .Base: copy, copyto!, axes | |||
export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__ | |||
export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, @: |
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.
Maybe export+document a renamed _lazy
as well? Hiding this functionality behind a macro only does not seem right. But the macro is really nice syntax!
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.
Thanks for reviewing the code!
I think it's better to avoid overly increasing the API surface. I don't see what exporting Lazy
enables you to do other than what is already possible with @:
. Also, implementing Lazy
can be done in a few lines so I don't think Base
should export it. (Note that it still makes sense for Base
to export @:
, even though it can be done in a few lines, because it let us use a common syntax across Julia ecosystem.)
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.
The main advantage of having a macro is nice looking source.
The cost of macros in general is increased complexity: It is not immediately clear what the AST is, what the types are, etc. Hence, readers of code need to either trust the docs (that don't tell you what they do on the AST), read the macro code, or macro-expand. In this specific case, the macro does not do anything complicated, and could be written as a lazy.(...)
. I am surely not the only one who tries to use macros sparingly, and I think lazy is important enough that it should be accessible to people who prefer view
and dot-syntax or even explicit broadcast
over @view
, @.
and the like.
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 agree on the point that it is ideal to provide non-macro interface for language features whenever appropriate. It certainly makes sense for normal functions. However:
-
Not every feature in Julia has "normal syntax" equivalent. For example,
@inline
,@inbounds
,@simd
, etc. do not have a straight-forward non-macro syntax. -
You can always run
@macroexpand
orMeta.@lower
to see what is going on in a given macro (or a specialized syntax). I think macros in Julia are very transparent. -
_lazy
does very unusual thing in the evaluation of the broadcasting expression (because that's its purpose). There has to be something that signals the readers that the evaluation rule is changed, even if they never used this feature. A macro is the best syntax for signaling this because that's what macros do.
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.
Fair enough.
@@ -1211,4 +1211,29 @@ end | |||
end | |||
@inline broadcasted(::S, f, args...) where S<:BroadcastStyle = Broadcasted{S}(f, args) | |||
|
|||
function _lazy end # only used in `@:` macro; does not have to be callable |
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.
_lazy <: Function
does not seem right.
struct _lazy end
and an exported const Lazy = _lazy()
with broadcasted(::_lazy, x) = (x,)
could be an alternative (users would write either sum(@: a .* b)
or sum(Base.Broadcast.instantiate(Lazy.( a.* b))))
.
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.
It does hold the place of a function, though. Either which way, it's necessarily a bit of a strange construct, so I'm not sure it really matters.
I don't want to see it exported for exactly that reason. It's odd (but necessary).
""" | ||
macro (:)(ex) | ||
return esc(:($instantiate(($_lazy.($ex))[1]))) | ||
end |
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.
Super cool syntax!
I like the fact that instantiate
is called before passing the object on. If a non-macro variant gets exported, I think that should eagerly instantiate
as well. That is, @inline broadcasted(::typeof(_lazy), x) = (instantiate(x),)
instead of instantiating in the macro? Or am I missing some reason that this cannot work?
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.
Yea, that's possible. Maybe better to do less in the macro. Another way is to create a custom wrapper object and instantiate
in materialize
, like you did in the other PR. But I'm seeing this as an implementation detail and it's better to use the simplest one.
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.
Unless _lazy
gets exported in some form, I agree.
@@ -1211,4 +1211,29 @@ end | |||
end | |||
@inline broadcasted(::S, f, args...) where S<:BroadcastStyle = Broadcasted{S}(f, args) | |||
|
|||
function _lazy end # only used in `@:` macro; does not have to be callable |
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.
It does hold the place of a function, though. Either which way, it's necessarily a bit of a strange construct, so I'm not sure it really matters.
I don't want to see it exported for exactly that reason. It's odd (but necessary).
@: broadcasting_expression | ||
|
||
Construct a non-materialized broadcasted object from a `broadcasting_expression` | ||
and [`instantiate`](@ref) it. |
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 see this as being akin to a generator; that is, generator : comprehension :: Broadcasted : broadcast
. Perhaps that could give us a bit of a better language around this? Maybe call it a broadcast generator?
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 agree that "non-materialized broadcasted object" is a bit too mouthful. It's nice to have a concise word for it.
broadcast generator
Broadcasted
is kind of like generators but I'm seeing the generators as a way to define iterators (i.e., limited forms of the coroutines). From this point of view, I find it somewhat confusing. But then Base.Generator
is just a lazy version of map
in Julia and it's pretty close to Broadcasted
. So maybe it's fine? I slightly prefer "lazy broadcast" although the term "lazy" is too overloaded.
generator : comprehension :: Broadcasted : broadcast
By the way, what do :
and ::
mean here? Maybe Base.Generator
corresponds to Broadcasted
and [f(x) for x in xs]
corresponds to broadcast(f, xs)
?
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.
Those colons aren't code... it's a (now antiquated) way of writing a chained analogy. For example puppy : dog :: kitten : cat can be read "a puppy is to a dog as a kitten is to a cat." Or in other words, the relationship between a generator and a comprehension is comparable to the relationship between Broadcasted and broadcast. I shouldn't have written it like that — someone would only have reason to know that if they specifically took a particular US college entrance exam before 2005. 😄
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, that's an interesting notation. Thanks for using it 😄
Isn't a |
It could be emulated by a generator with an appropriately constructed iterator and a splat, sure. A zip can easily handle the multiple items, but part we don't have is the definition of the outer shape. Also generators work by iterating, broadcast works by indexing, but again, that could be emulated in the generator's iterable. The big downside with using a generator is that it loses the indexability, which is a very nice feature. Cf. #31020. |
Hmm. Well why not just modify Zip to be better at figuring out outer shapes, and modify both to handle indexing appropriately? |
Or just to clarify, there's really only 2 semantic concepts here: zipping and mapping. So all we really need is a lazy |
What would this gain us? If we want to expose this to users as a generator, then we can simply return There are only two key questions for this PR:
For what it's worth, I'm pretty happy with the current state on both those points. |
@bramtayl There is also broadcasting, right? I mean, |
Yes, exactly. We can use |
Yup sorry to distract from the PR. Just some things to think about. |
Allowing |
I commented on why I thought |
Triage proposal: A key observation here is that we could avoid materializing broadcast in iteration contexts, so e.g.
could just iterate over the broadcasted object directly. That would mean that e.g. |
This is a very clever observation! Is By the way, it's not crucial, but regarding:
Are you planning to implement this? I guess it's just a hypothetical feature (as you said could)? I think this would change the result when the broadcasting includes side-effect like this: rng = MersenneTwister(0)
acc = 0.0
for x in rand.(Ref(rng)) .+ zeros(3)
acc *= rand(rng)
acc += x
end But, it's irrelevant for the additional syntax |
It's implemented in #31553. |
What if you want both |
FYI |
Triage thinks this is a good feature with too obscure a name.
|
This is an experimental patch to allow
@:
as a macro syntax and then use it to construct a non-materialized broadcasted object. Let's keep the discussion about the syntax in #19198.