New to Julia, so please have patience if I am missing something in this suggestion.
Since the .=
operator is used for in-place assignment, it does not currently make sense to define a function using this operator. That is to say, the statement f(x) .= 3+4x
would not have any meaning. The statement and variations such as f .= x -> 3+4x
indeed trigger strange errors.
I think it would be elegant to hijack the .=
operator when the LHS is a function definition or RHS is a lambda; my hijack proposal is to make f(x) .= 3+4x
and/or f .= x -> 3+4x
into f(x) = @. 3+4x
or something similar. This is less in-line with the behavior of .=
as an "in-place assignment" operator, and more in-line with the dot as a broadcast operator.
The standard way to do this kind of things in julia is to define functions for scalars, then vectorize: f(x) =2x+3;f.(X)
if X is an array. Does this not do what you want?
Thinking: _"Limit argument types to Array{Any,N} by dot-before-equal-sign. Which syntactic sugar could be assigned to execute f(x::Scalar)? Sole "@" macro!"_
Measures taken wouldn't improve readability for me.
Anyway, code can be reprocessed with some macro just to translate your new definitions to current standard, @alhirzel .
Defining "vectorized" functions (rather than using explicit "dot calls" f.(array)
) is actively frowned on these days. The advantage of dot calls is that they fuse with other dot calls into a single loop.
That is, if you define a vectorized (elementwise) function f(array)
, then if you do 2 .* f(array) .+ 1
your f(array)
call will allocate a temporary array and perform a separate loop. Whereas if you define only f(scalar)
then 2 .* f.(array) .+ 1
or @. 2f(array) + 1
would allocate only a single array and fill it in with a single loop.
See also #17302 — we used to have a special macro for defining vectorized methods, which was removed for precisely this reason.
Thank you for the info. I did not previously have this clear of a picture. If the Julian style is to define scalar functions and promote, then I agree the .=
operation for creating broadcasted functions is unmotivated. I wondered how one would keep track of which functions are "internally dotted" and which ones aren't, but if I summarize: this is resolved by assuming you broadcast at the vector call and all functions operate on scalars until you see a dot. If you want to avoid "along-the-way allocations", this is done by convention, and you assume there is sensible or no internal broadcasting for functions when you call them.
The only gap is behavior of the .=
operator for function definition or lambdas. I don't know if it would be in best style to error out, but it is not intuitive what this operator would do from the "in-place assignment" perspective when applied to function arguments (as in the f(x) .= 3x+4
example). Any thoughts on what this should do? Here is a specific REPL session that is unintuitive IMO:
julia> f(x) .= 3x+4
ERROR: UndefVarError: f not defined
Stacktrace:
[1] top-level scope at none:0
julia> f(x) = 3x+4
f (generic function with 1 method)
julia> f(x) .= 5x+6
ERROR: UndefVarError: x not defined
Stacktrace:
[1] top-level scope at none:0
julia> f .= x -> 5+6
ERROR: MethodError: no method matching size(::typeof(f))
Closest candidates are:
size(::BitArray{1}) at bitarray.jl:70
size(::BitArray{1}, ::Any) at bitarray.jl:74
size(::Core.Compiler.StmtRange) at show.jl:1561
...
Stacktrace:
[1] axes at ./abstractarray.jl:75 [inlined]
[2] materialize!(::Function, ::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{0},Nothing,typeof(identity),Tuple{Base.RefValue{getfield(Main, Symbol("##3#4"))}}}) at ./broadcast.jl:759
[3] top-level scope at none:0
f(x) .= 3x+4
currently means broadcast!(identity, f(x), 3x+4)
. That is,it calls broadcast!
and writes the output to the result of calling f(x)
.
This is actually potentially useful behavior if f(x)
is some special array-allocation function. For example:
foo(x) = sort!(myallocate(length(x)) .= x .+ 1) # several in-place operations on result of myallocate(n)
(Changing this into a new kind of function definition would be breaking, and hence cannot happen in Julia 1.x.)
With your other example, f .= x->…
, that's doing broadcasted assignment of the anonymous function _into_ f
, which must already exist as a mutable collection. That's why it's erroring in your example — you're trying to assign into a function that you've already defined! .=
is different from straight =
assignment as the left-hand-side must _already exist_ and so it doesn't introduce a new binding. It just modifies something… so you're able to put _any_ expression there and it'll modify it.
Given that we all agree that a "vectorized function definition" isn't really needed and that we have a defined meaning here, I think we can close this.
Yep agree on closing; thank you for answering my questions!
Most helpful comment
Defining "vectorized" functions (rather than using explicit "dot calls"
f.(array)
) is actively frowned on these days. The advantage of dot calls is that they fuse with other dot calls into a single loop.That is, if you define a vectorized (elementwise) function
f(array)
, then if you do2 .* f(array) .+ 1
yourf(array)
call will allocate a temporary array and perform a separate loop. Whereas if you define onlyf(scalar)
then2 .* f.(array) .+ 1
or@. 2f(array) + 1
would allocate only a single array and fill it in with a single loop.See also #17302 — we used to have a special macro for defining vectorized methods, which was removed for precisely this reason.