julia icon indicating copy to clipboard operation
julia copied to clipboard

Allow function negation `-f` to work like `!f`

Open mcabbott opened this issue 1 year ago • 4 comments

We have !f === (!)∘f to negate any boolean function. Was there ever discussion of making -f === (-)∘f too? Both are small conveniences to avoid defining x -> op(x). This one would allow for example:

julia> A = reshape(1:16,4,4);

julia> eigvals(A; sortby=-real)  # does not accept rev=true
4-element Vector{ComplexF64}:
     36.209372712298546 + 0.0im
 -4.146487327145629e-17 + 1.9341169324764503e-16im
 -4.146487327145629e-17 - 1.9341169324764503e-16im
     -2.209372712298546 + 0.0im

julia> sum(-log, A; dims=1)  # -sum allocates twice
1×4 Matrix{Float64}:
 -3.17805  -7.42655  -9.38261  -10.6846

This will not change the fact that -f.(xs) means -(f.(xs)), when usually fused @. -f(xs) would be better. (Unless you write (-f).(xs) by hand.)

Needs tests obviously, but RFC?

mcabbott avatar Sep 28 '24 16:09 mcabbott

I'm not necessarily opposed to this but I'll raise some downsides.

(1) Defining functionality for functions that doesn't work on general callables means we can't use it in functions that take generic callable argument.

(2) Related, -a is already syntax for AbstractArray while !a isn't, so a callable AbstractArray might have confusing behavior if the -a is different under function application versus indexing. I don't know of any examples like that but there might be some.

(3) I don't like special case syntax: I think it would be better if instead of adding !f and -f to avoid the extra characters in (!)∘f, we could have instead !∘f and -∘f -- without the parentheses -- that would just work and be almost as concise as the special cases !f and -f.

jariji avatar Sep 28 '24 19:09 jariji

Do the same for +, out of symmetry?

eschnett avatar Oct 09 '24 22:10 eschnett

personally speaking I don't think I'm a fan of this. the ("wat" potential) : (cuteness of syntax gained) ratio is too high

adienes avatar Oct 09 '24 23:10 adienes

Point (2) above is an interesting deviation from how !f works. It's true that ![true, false] is an error, while -[1,2,3] is the same as broadcasting. So it would be somewhat strange if (-a)(i) != -(a(i))... assuming that (-a) were defined like this PR, and that the array is callable.

The only callable array I can think of is from my package, which uses (ab)uses round brackets to mean another kind of indexing. In this case, the two meanings would agree, since indexing the negated array -A is the same as indexing and then negating:

julia> A = AxisKeys.KeyedArray([10, 20, 30], char='a':'c');

julia> A('b') == A[2] == 20  # 2nd element can be accessed by its label 'b'
true

julia> -A('b') == ((-)∘A)('b') == -20
true

The closest related PR is probably #39042, which makes f^2 === f∘f. That has a different overlap of ideas with callable arrays, in that A^2 == A*A for matrices. Note that if both PRs were merged, the parser thinks the minus is outside: -3^2 == -9.

I can't picture why defining +f would be helpful. Certainly there is general intention to call arbitrary functions on other functions, g(f) === g∘f. It is conceivable that some people would want f-g to mean x -> f(x)-g(x), and similarly f+g, but those seem a big step further, and this PR does not propose them.

Maybe it would be worth thinking up the worst "wat" which this change would allow?

mcabbott avatar Oct 15 '24 22:10 mcabbott

From the point of view of generic programming, !f shouldn't have been added in the first place. The meaning of a generic function should not change depending on the type of the value that is passed to the function. The expression !x normally means "negate the logical value x". But for x::Function it means (!) ∘ x.

Likewise, we should not have two separate meanings for unary -:

-x == -1 * x

-x == (-) ∘ x   # If `x` is a `Function`. 

CameronBieganek avatar Nov 08 '24 02:11 CameronBieganek