Iâm necro-ing this thread because I opened https://github.com/JuliaLang/julia/issues/28961
, and I feel it would be better to have the âphilosophicalâ discussion collected in one place. To be clear; I am discussing the philosophy of broadcasting, completely independent of any @.
concerns. This is purely about what .(
should âmeanâ. Iâll state my thesis first, then respond to the most pertinent comments from Jeff and Matt above.
I think the problem is that broadcasting is supposed to split its behavior depending on whether something is a âscalarâ or not. In my opinion, this should be a binary decision; something is either a scalar or not. Flipping that logic, I would rather state something is either a container or it is not. The .(
operator applies the âmap operation over elements of a containerâ operation, with the escape hatch of operating upon a scalar as a single-element container. I believe that escape hatch is very important as it makes writing recursively-broadcast methods much easier (for example, mapping elements of tree structures).
With this mindset, we have to decide how to define something as a âscalarâ. So far in Julia land, weâve pretty much done this through multiple dispatch; call length(x)
, or iterable(x)
or something like that. As Jeff notes above, this gives the problem that a new package bringing in new method definitions could cause âaction at a distanceâ and suddenly change the broadcasting behavior for my type. We could argue about whether or not that is a feature or a bug, but since we are talking about the fundamental capabilities of a datatype, I think this is the kind of thing that should be solved by something tied to the datatype definition; e.g. a trait. Everything that is not marked Iterable
is treated as a scalar.
With that stated, here are my responses to the two comments from May 29th:
The first problem is that itâs useless for f.(x)
to be equivalent to f(x)
â you bothered to write a dot, so we ought to do something with it.
I would agree with this if there werenât so many examples precisely where f.(x) = f(x)
. IntX
, FloatX
, Symbol
, Char
, Ptr{X}
, etc⌠there are many examples where this is the case, and I argue that this is a good thing; otherwise we should throw an error on f.(1)
. Itâs difficult to write generic code that should work on both containers and scalars without this fundamental capability.
Itâs also conceivable that a type could add or remove an iterate
method, and that would silently change the behavior of your program.
I agree this is a serious concern; this is why I think we need a way to tie dispatch to âintrinsic informationâ about a datatype, rather than methods that can always be added later. I do not think that allowing package B to change whether a datatype defined in package A is Iterable
or not is really something we desire; we would want the datatype definition itself to define whether it is Iterable
through some kind of trait, and thus we could use that same declaration here.
By defaulting to errors for non-iterables, itâs a great big signal for library creators that they can opt-in to acting like a scalar if it is appropriate
Conversely, by defaulting to treating everything as a scalar, it allows all code to ârunâ, and if the library author actually wants something to be Iterable
it is stated within that package. I donât see why throwing an error is ever desirable in this case.
End-users can opt-in to scalar-like behaviors by wrapping in another broadcastable container
Wrapping in Ref()
is a good solution for disabling broadcasting, I agree.
Were we to default to treating everything like a scalar, that means that they would have to collect an iterable into a different broadcastable structure in order to iterate over it.
I think youâre saying here to treat everything as a scalar, but instead I want to treat everything that does not contain the Iterable
trait, which is slightly different. There would be no way to âopt-inâ to broadcasting, as that requires explicit support, but there is always a way to opt-out, which already exists; Ref()
.
Even with base itself, this led to lots of surprises â some folks wanted to iterate over sets but treat dicts as scalars. Or broadcasting the array-ish Linear Algebra I
object was quite a surprise.
All of this is solved by being consistent and supporting the Ref()
disabling of broadcasting.