Julia: RFE: syntactic sugar f(x) .= 3+4x for vectorized function definition

Created on 27 Dec 2018  Â·  7Comments  Â·  Source: JuliaLang/julia

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.

broadcast speculative

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 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.

All 7 comments

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!

Was this page helpful?
0 / 5 - 0 ratings