Skip to content
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

Parenthesis Postfix Function Syntax Allowing (x,y)f #38704

Closed
jessebett opened this issue Dec 4, 2020 · 40 comments
Closed

Parenthesis Postfix Function Syntax Allowing (x,y)f #38704

jessebett opened this issue Dec 4, 2020 · 40 comments
Labels
kind:breaking This change will break code

Comments

@jessebett
Copy link
Contributor

This issue is to discuss the possible advantages and disadvantages for allowing a postfix function call syntax where the function name appears after the closing parenthesis.
i.e. (x)f or (x,y)f

I think this notation has merit because it maintains the relationship between parenthesis() and function calling.

Recall that, by definition, Tuples are an abstraction containing the arguments of a yet unspecified function: (x,y).
f(x,y) gives the syntax to call the function f on the tuple of arguments.

I propose we extend that intuition to also allow (x,y)f to be valid syntax for function calling.

Right now it is not valid syntax. Further, (x)f is parsed as juxtaposition, so *. This causes f(x) to behave completely differently from (x)f. I don't think that is ideal either.

We would have to resolve some ambiguities e.g. f(x)g and (f)(g).

Are there obvious problems with allowing this beyond aesthetic discomfort?

@StefanKarpinski
Copy link
Sponsor Member

It should be noted that this is so unsettling to @ararslan that he hasn't even thrown his customary 👎🏻 on it, but merely put a 😕 on it instead.

@jessebett
Copy link
Contributor Author

"aesthetic discomfort" was a pointed comment aimed at @ararslan

@ararslan
Copy link
Member

ararslan commented Dec 4, 2020

I thought it deserved a 😕 given that the issue doesn't actually articulate why one would ever want to do this and what the perceived benefit would be.

@oxinabox
Copy link
Contributor

oxinabox commented Dec 4, 2020

I will point out we have AFAIK exactly 1 post fix operator.
' which defaults to adjoint.

I would maybe like some more postfix operators.
But I don't know that I would want postfix functions that require (,)

@jessebett
Copy link
Contributor Author

jessebett commented Dec 4, 2020

The issue is missing some context from discussions on Slack around issues that result from juxtaposition.

One potential benefit is that it feels wrong that (x)f should lower to *. This would prevent that.

@jessebett
Copy link
Contributor Author

A less provocative strategy for this issue could have been this:

Regardless of whether (x)f should be postfix notation, why should this syntax involve a method error on * at all? To me this is much more unsettling than the existence of postfix notation (which will never not upset @ararslan ) and as @oxinabox already pointed out, we have a postfix in Julia already.

f = sin
x = 2

(x)f 
# MethodError(*,(2,sin))

@KristofferC
Copy link
Sponsor Member

why should this syntax involve a method error on * at all?

Because that's how the syntax is documented (https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#man-numeric-literal-coefficients).

If you want to get rid of this syntax, then open an issue about that instead perhaps?

@jessebett
Copy link
Contributor Author

jessebett commented Dec 4, 2020

@KristofferC my syntax issue has nothing to do with integers or floating point numbers.

x = sin
y = cos

x(y) #MethodError(sin, (cos,))
(x)y #MethodError(*, (sin, cos))

First method error makes sense. Second method error involves * because juxtaposition is lowered to multiplication.

@KristofferC
Copy link
Sponsor Member

KristofferC commented Dec 4, 2020

I didn't say it did. Read further in the section of the docs I linked:

Additionally, parenthesized expressions can be used as coefficients to variables, implying multiplication of the expression by the variable:

julia> (x-1)x

@mbauman
Copy link
Sponsor Member

mbauman commented Dec 4, 2020

One potential benefit is that it feels wrong that (x)f should lower to *. This would prevent that.

One potential downfall is that (x)y already means *. This would be breaking.

@jessebett
Copy link
Contributor Author

I did read it and understand that it is where this syntax is documented. I am saying that the behaviour of (x)y should not involve multiplication at all. Let alone being documented in the section on Integers and Floating Point Numbers.

If you want to get rid of this syntax, then open an issue about that instead perhaps?

I don't want to get rid of the syntax. I want the syntax to mean something intuitive, which is why I opened this issue.

@Seelengrab
Copy link
Contributor

Seelengrab commented Dec 4, 2020

What would foo(bar)baz even mean with this? Julia doesn't support arbitrary precedence of functions, so what should happen? Parser error? First call baz with argument bar, then foo with argument bar or the other way around? You could make a case for either one of them, so to fix this in place would need REALLY good convincing arguments.

To me this seems like a syntax change that just wants to extend tuple semantics for the sake of expanding them..

@ararslan
Copy link
Member

ararslan commented Dec 4, 2020

I want the syntax to mean something intuitive

(1)sin meaning sin(1) feels more "wat" than intuitive, IMO.

@jessebett
Copy link
Contributor Author

@ararslan It's not surprising that familiar prefix functions like sin feel like "wat" when written postfix.

But is the current situation actually intuitive? That (foo)bar == foo * bar in all cases. But foo(bar) == foo*bar only when foo is a numeric literal? To me it does not.

Also. I didn't open this issue to unnecessarily propose breaking changes just for fun. The existing syntax surrounding juxtaposition and multiplication is a regular gotcha. It started a discussion on gripes channel about surprising, unintuitive results. e.g.

julia> xf = 10
10
julia> 2xf
20
julia> 0xf
0x0f

Trying to resolve these annoying juxtaposition issues naturally arises at the question of the correct behaviour for this syntax.

@KristofferC KristofferC added the kind:breaking This change will break code label Dec 4, 2020
@jessebett
Copy link
Contributor Author

jessebett commented Dec 4, 2020

@mbauman yes this is definitely breaking. Here's the specific behaviour right now.

Any expression that is not a numeric literal, when immediately followed by a parenthetical, is interpreted as a function applied to the values in parentheses.

So if we follow the spirit of this it creates the cases of (x)y where y is a numeric literal and when it is not.
If (x)y == x*y when y is a numeric literal it would keep the current behaviour for those cases.

Do we have a sense for what would break (x)y where y is not a numeric literal?

@Seelengrab
Copy link
Contributor

Seelengrab commented Dec 4, 2020

Really not sure if I should share this, but you don't need to change the parser to achieve this behaviour:

julia> g(x) = x << 2
g (generic function with 1 method)

julia> Base.:*(a, ::typeof(g)) = g(a)  # please don't actually do this

julia> 4g
16

julia> a = 6
6

julia> (4a)g
96

julia> g(4a)
96

@mbauman
Copy link
Sponsor Member

mbauman commented Dec 4, 2020

Do we have a sense for what would break out of those cases?

Lots. Quick and dirty grep; it's pretty shoddy and picks up lots of string interpolations and comments, but a large number of real cases. I didn't see one post-fixed numeric literal, but barely made it through the "B" packages.

a1 = sqrt(3)L0.*(cos/6), sin/6))
p0 = [[cos(θ)L0, sin(θ)L0] for θ = range(0, stop=5π/3, length=6)]
(6x/L^2 - 6x^2/L^3)u0[5] +
C = transpose(F)F
xy'' ++ 1 - x)y' + ny = 0
@test minutes(s) == (1.0 / 60.0)minutes
return -(nmd/2)*log(2pi)-(1/2)log(det(σ²))-(1/2)*diff'*σ²^(-1)*diff
@test A∧⋆(B) == (AB)pseudoscalar(A)
...

@darsnack
Copy link

darsnack commented Dec 4, 2020

There are two pieces to this.

  1. Juxtaposition is always parsed and lowered to *.
  2. Postfix notation when a tuple is juxtaposed with a function.

Just repeating what I said in the Slack thread:

  1. Instead, we could lower to juxtapose(x, y) for (x)y. The best argument I have seen for juxtaposition at all is Unitful.jl. Having a defined juxtapose function to extend would be cleaner than extending *.
  2. Given (1), should juxtapose(t::Tuple, f::Function) = f(t)?

I think this issue could benefit from more examples of domains that would benefit from postfix (I know there exists stuff in math/category theory, but I am no expert). (1) is one of those things that I wish had been done this way from the start, but it already lowers to * so 🤷 .

@mbauman
Copy link
Sponsor Member

mbauman commented Dec 4, 2020

(1) is one of those things that I wish had been done this way from the start, but it already lowers to * so 🤷 .

I like (1), and that wouldn't be breaking. We can always add levels of indirection for folks to opt into.

@darsnack
Copy link

darsnack commented Dec 4, 2020

Yeah, part of my argument is that postfix can be achieved for specific packages that need it if juxtaposition being extended was "official" and not "clever." Circumventing the issue of forcing postfix calls everywhere.

@Seelengrab
Copy link
Contributor

Seelengrab commented Dec 4, 2020

I don't like juxtaposition as function call syntax in general, but I agree that juxtaposition of numeric literals is fine to me (since it does save the odd character). I'm happy with the status quo.

Yeah, part of my argument is that postfix can be achieved for specific packages that need it

You can do that already by only extending * for the functions/types from your package. If your type already has an implementation for * it works already anyway and if you want to juxtapose functions - what else would be meant by juxtaposing the literal function itself? There is no need to complicate every bit of code out there for this.

One painpoint I see is something like (var1)func(var2) - which should be called first? And if you lower it to some multi-argument call to juxtapose, does the order suddenly matter for the function? I can see people having to implement juxtapose(var1, func, var2), juxtapose(var1, var2, func).... That's hardly easier to do. You'd have to define precedence again, which would be an interesting (albeit very much a complicating) addition. The parser doesn't know what's a function and what isn't.

@jessebett
Copy link
Contributor Author

I don't like juxtaposition as function call syntax in general

It's actually () that is function call syntax. Tuples are just (x,y) syntax for an unspecified function called on arguments (x,y). So, critically, it is juxtaposition with () that is the function call syntax in Julia: f(x,y).

This issue is to unify that with the behaviour of juxtaposition with () against the right, (x,y)f.

@darsnack
Copy link

darsnack commented Dec 4, 2020

The parser doesn't know what's a function and what isn't.

Why does this precedence breaking need to be done by the parser? All the parser does is output the tree juxtapose(var1, func, var2) (or juxtapose(juxtapose(var1, func), var2) if you want to force L -> R ordering). The parser already knows how to "see" juxtaposition and call *. The only difference here is that it calls juxtapose instead of *.

I don't really see why Base would need to spend effort making precedence decisions either. If what's being juxtaposed are numbers, then follow the current behavior. Otherwise just throw a method error. I would argue against defining juxtapose(t::Tuple, f::Function) in Base, because it isn't clear what's the right thing to do in (x)f(y) cases.

@Seelengrab
Copy link
Contributor

Seelengrab commented Dec 4, 2020

because it isn't clear what's the right thing to do in (x)f(y) cases.

Precisely, so each package would make it's own decisions about the order this is done in. From a user perspective, this seems like a horrible idea, since now I have to learn additional precedence rules for each package I want to use because each package is free to define them as they wish, by choosing how to handle each juxtapose case.

@rapus95
Copy link
Contributor

rapus95 commented Dec 4, 2020

Once we define juxtaposition between an operator and any other type we can't have infix operators anymore since we'd have to decide on a precedence in parsing:

(2+4)*2 --> (((2+)4)*)2

which, if we removed the juxtaposition->* rule would suddenly error because we can't juxtapose 2 and 4 or otherwise produce the result 16 (=(((2+)4)*)2==(((2)4)*)2==((8)*)2==(8)2==16)

@darsnack
Copy link

darsnack commented Dec 4, 2020

Yeah that's a good point! We could apply the same restrictions of infix operators as we currently do though. In which case infix operators must explicitly be wrapped in parentheses to count as being juxtaposed (e.g. (4)(*) is juxtaposition but (4)* is not).

Though I know there are people who want to use infix operators with juxtaposition to express some mathematical operations.

@jessebett
Copy link
Contributor Author

jessebett commented Dec 4, 2020

@oxinabox that we already have one postfix operator and it is adjoint shouldn't be surprising in the context of Automatic Differentiation. If the Jacobian J is the linearization of a program. The Jacobian-vector product Jv can be thought of as the linear program taking in the input perturbations as an argument J(v).

But the linear program, when transposed, Jt = J', is also a linear program that accepts a vector of output perturbations Jt(v). This is called vjp "vector-Jacobian" product. Why? Because we can rearrange the expression to v'J.

If we had postfix notation this would very elegantly be written as (v')J.

This is my attempt at coming up with a realistic example of a postfix function. Incidentally the fact that J(v) == J*v is a comment on the linearity of the function J, that it can be represented as a matrix.

@jessebett
Copy link
Contributor Author

Also, @oxinabox, I think we will get another postfix operator for transpose from the PR that resulted from #21037

@KristofferC
Copy link
Sponsor Member

It's actually () that is function call syntax. Tuples are just (x,y) syntax for an unspecified function called on arguments (x,y).

() is syntax for tuples, f() is syntax for call:

julia> dump(:((1,2)))
Expr
  head: Symbol tuple
  args: Array{Any}((2,))
    1: Int64 1
    2: Int64 2

julia> dump(:(f(1,2)))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol f
    2: Int64 1
    3: Int64 2

Just like [] is syntax for vect and T[] is syntax for ref.

@jessebett
Copy link
Contributor Author

() is syntax for tuples, f() is syntax for call

Yes, thank you for the correction. It was not correct that () that is function call syntax.

What I was trying to appeal to is that the connection between Tuples, and their syntax () and function calls f() is much deeper than just that they both involve parenthesis.

From the Julia Documentation on Tuples

Tuples are an abstraction of the arguments of a function – without the function itself.

So the involvement of the parenthetical () in function call is the same as the reason parenthesis wrap a Tuple.

It's true that parenthesis collect expressions for specifying precedence. In grade school, when we only knew about * and +, this allowed for a convenient shorthand notation where we can exclude the *. It's syntactic sugar. We've kept this legacy, I think mostly for the same aesthetic appeals to make math more terse.

So, whereas () is syntax for tuples, f() is syntax for call feels like a very deep and intentional use of () to mean something related to a function's arguments, the fact that ()f would lower to a * feels like an accident. I understand that this accident is useful for terse math, but is that a good enough reason?

Just like [] is syntax for vect and T[] is syntax for ref.

Julia> T = [1,2,3]
Julia> [1]T
ERROR: MethodError: no method matching *(::Array{Int64,1}, ::Array{Int64,1})

Again, why should this syntax involve multiplication? To be clear, if juxtaposition between two types really does behave like *, then that is fine. But the current situation, that almost always juxtaposition is multiplication, except for prefix, except for numeric literals.... It does not feel very coherent.

Is it a breaking change if we lower juxtaposition to it's own function juxtaposition and then supply those methods that maintain the existing, convoluted behaviour? I.e., make the relevant methods dispatch to *.
This would give opportunity for packages like Unitful to make use of juxtaposition without relying on providing yet more methods to *. (Though unfortunately for my argument here I think units are a good candidate for juxtaposition to really mean *).

@JeffBezanson
Copy link
Sponsor Member

JeffBezanson commented Dec 5, 2020

Lowering to juxtapose instead of * might be ok. But if there is a fallback that calls *, you will still get a method error about *. If it does not fall back to *, then it's breaking. Either way, that change is only helpful if we want one or both of:

  1. Types where juxtaposition and multiplication behave differently. Doesn't seem desirable to me.
  2. Types that define juxtaposition, but not multiplication. Maybe ok for units?

Is the proposal to use (2) to make f(x) call juxtapose(f, x), and then define e.g. juxtapose(f::Function, x) = call(f, x)? If so, I'm very much against that. In general, the later in the pipeline you make these kinds of semantic distinctions, the more confusing things are. It would conflate function call and multiplication in a way that we simply don't today. It's fair to ask "why is (x)f multiplication?", but at least it's always multiplication.

But the current situation, that almost always juxtaposition is multiplication, except for prefix, except for numeric literals....

Those aren't "exceptions" in the same sense, because most of the exceptions are actually syntax errors. Syntax errors are perfectly safe, because there's no risk of giving something the wrong meaning. And f(x) and a[i] are such firmly established notations that nobody is genuinely confused about why they aren't considered juxtaposition.

Julia> [1]T

I'd be perfectly happy to remove this case of juxtaposition.

@FelixBenning
Copy link

FelixBenning commented Dec 7, 2020

Potential benefit: better autocomplete

In object oriented programming language you get very easy method discovery with object.| expanding to an autocompletion list. This makes working with apis in object oriented programming languages a breeze. Currently you always have to google to find methods - or remember that function (methodswith(?)). Anyway - this could get a little closer to that:

(object|

might expand to suggestions. It isn't quite as powerful as object.| since you can do chainging there object.method().| and get the next batch of suggestions, while

(object)method|

requires you to go back and add a bracket. But it still gives you one level of autocompletion. That would put it on par with python, which can only give one level since it is not statically typed so autocomplete can not help with the return value.

@JeffBezanson
Copy link
Sponsor Member

If the extra cursor movement is ok, we could also do |(object) (tab before an argument list).

@FelixBenning
Copy link

@JeffBezanson a good autocomplete is not something you should need to know/think about. So if you have to move around your cursor and press tab, it isn't really all that useful. It should just come up naturally while you type what you need to type anyway.

If you have to do a certain thing, then knowing that certain thing is somewhat equivalent to knowing a methodswith function. It isn't really making these things any more discoverable.

@ararslan
Copy link
Member

ararslan commented Dec 7, 2020

I don't think being able to tab-complete a function name postfix on its arguments actually makes anything more discoverable given that you don't call it that way.

@FelixBenning
Copy link

@ararslan currently yes, but I thought this was about allowing for this syntax (x,y)f. And in the case of multiple dispatch I think this might be a more suitable notation for autocompletion. Or at least that is a possible benefit.

@fingolfin
Copy link
Contributor

As a mathematician working in abstract algebra, specifically group theory. We use right actions all the time, and apply functions from the right; in the computer algebra system GAP (of which I am a developer) we offer the shorthand x^f for this, which is inspired by the historic notation for function applications (which predates f(x)).

Despite being used to applying functions such permutations from the right all the time, I am not really in favor of the syntax (x)f. Indeed, I find it confusing and I think it would make Julia code even harder to read to people new to it. I.e., while it may seem "cute" and have "aesthetic appeal" the cognitive load it induces does not seem worth it to me; it's overly "clever" but not really "smart".

That's of course just my opinion and purely subjective! The only reason I bring it up is that because people here mentioned that research mathematicians might appreciate it. I am sure some will, but some (like me) won't :-).

@fingolfin
Copy link
Contributor

(BTW, we also apply matrices from the right, xA=b, and prefer row over column operations, so I am already used to Julia doing things differently than I'd prefer :-), so no worries about that )

@jessebett
Copy link
Contributor Author

The tab completion is an interesting aspect and I'm glad @timholy gave it it's own issue!

@fingolfin that we "apply" matrices on the right or the left is exactly the abstraction I want to emphasize. Matrices are representations of linear programs whose call signature happens to be exactly multiplication, *. Applying a matrix from the right is identical to calling the transposed matrix on its transposed arguments: xA = (A'x')' = (A'(x'))'.

Trying to appeal to a notion that generalizes linear program "calling", i.e., matrices and matrix *, to nonlinear program calling, i.e., generic functions f and their call syntax f().
Since we currently have linear programs "called/applied" from the right x*A I'm suggesting this behave similarly to (x)A.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Apr 3, 2021

I think as a breaking change, we aren't likely to do this. Further discussion can continue, or be had on discourse. Tab-completion for methodswith will be added in #38791

@vtjnash vtjnash closed this as completed Apr 3, 2021
timholy added a commit that referenced this issue Jul 25, 2021
timholy added a commit that referenced this issue Aug 9, 2021
* ?(x, y)TAB completes methods accepting x, y

Closes #30052
xref #38704
xref #37993

Co-authored-by: Jameson Nash <vtjnash@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind:breaking This change will break code
Projects
None yet
Development

No branches or pull requests