Syntax about *(a, b)

I think it is reasonable for users to write code in the latter style, but it errors, can this be improved?

julia> [2*3 3*4;
       3*4 2*3]
2Γ—2 Matrix{Int64}:
  6  12
 12   6

julia> [*(2, 3) *(3, 4);
       *(3, 4) *(2, 3)]
ERROR: MethodError: no method matching *(::Int64, ::Tuple{Int64, Int64})
The function `*` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  *(::Any, ::Any, ::Any, ::Any...)
   @ Base operators.jl:596
  *(::Real, ::Complex{Bool})
   @ Base complex.jl:330
  *(::Integer, ::CartesianIndex{N}) where N
   @ Base multidimensional.jl:129
  ...

Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

This is some really dumb parsing. It’s parsing as *(2, 3) multiplied by the tuple (3, 4).

A better example is

julia> [1 *(2, 3)]
ERROR: MethodError: no method matching *(::Int64, ::Tuple{Int64, Int64})

Closest candidates are:
  *(::Any, ::Any, ::Any, ::Any...)
   @ Base operators.jl:587
  *(::Real, ::Complex{Bool})
   @ Base complex.jl:327
  *(::Number, ::Missing)
   @ Base missing.jl:124
  ...

Stacktrace:
 [1] top-level scope
   @ REPL[4]:1

The binary operator * has a higher precedence than the white space that defines the matrix.

julia> f(x, y) = x * y
f (generic function with 1 method)

julia> [1 f(2, 3)]
1Γ—2 Matrix{Int64}:
 1  6

The solution is to use () to disambiguate what you want:

julia> [1 (*(2, 3))]
1Γ—2 Matrix{Int64}:
 1  6

You can argue about whether the current behavior is desirable, but changing it would be a breaking change so it’s not going to happen.

10 Likes

This example indicates that the infix usage is the standard way to call *.
The prefix usage (i.e. function-like api) of * is thus rendered somewhat questionable.
Or, if we want the prefix style, we will have to write Base.:*(a, b)

But this point is not reflected in the docstring (which seems the usage *(a, b) is formal and standard). I think this point needs to be stressed.

1 Like

You could also use the ;; syntax, like this:

julia> [1;; *(2, 3)]
1Γ—2 Matrix{Int64}:
 1  6

@WalterMadelim, for the matrix example you can, do

julia> [*(2, 3); *(3, 4);;
        *(3, 4); *(2, 3)]
2Γ—2 Matrix{Int64}:
  6  12
 12   6

Note that you have to change the order of the elements relative to the standard syntax, so [a; c;; b; d] is equivalent to [a b; c d].

See: Single- and multi-dimensional Arrays Β· The Julia Language

5 Likes

Nice, I’ve learned that.

julia> [1 2; 3 4]
2Γ—2 Matrix{Int64}:
 1  2
 3  4

julia> [(1;; 2); (3;; 4)] # this intention of specifying precedence is wrong
2-element Vector{Int64}:
 2
 4

julia> [[1;; 2]; [3;; 4]] # instead, this is correct
2Γ—2 Matrix{Int64}:
 1  2
 3  4

julia> [1; 3;; 2; 4] # correct
julia> [[1, 3] [2, 4]] # correct
julia> [[1, 3];; [2, 4]] # correct
1 Like

I would disagree with this. Prefixing infix operators is not idiomatic Julia style, unless you want to define a method etc.

1 Like

But the docstring suggests the validity of this usage, in abundance

  *(x, y...)

  Multiplication operator.

  Infix x*y*z*... calls this function with all arguments, i.e. *(x, y, z, ...), which by default then calls (x*y) * z * ... starting from the left.
...

  Examples
  ≑≑≑≑≑≑≑≑

  julia> 2 * 7 * 8
  112

  julia> *(2, 7, 8)

Another example

help?> ∈
"∈" can be typed by \in<tab>

search: ∈

  in(collection)
  ∈(collection)

  Create a function that checks whether its argument is in collection, i.e. a function equivalent to y -> y in collection. See also insorted for use with sorted   
  collections.

  The returned function is of type Base.Fix2{typeof(in)}, which can be used to implement specialized methods.

  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  

  in(item, collection) -> Bool
  ∈(item, collection) -> Bool

It is totally valid! Not idiomatic though.

As Oscar explained, [1 *(2, 3)] is parsed as [(1 * (2, 3))]. For the time being, you can keep using the more common infix notation, which does not have this issue, or place more parentheses to clarify your intention: [1 (*(2, 3))].

1 Like

Yes, it is certainly valid, as all infix operators have a prefix call form.

It is just not idiomatic in general. Certainly not for the case in your example: each * has two operands, and they are constants so they can be folded, and the whole operation is rather cheap.

But my biggest concern would be code readability. Yes, seasoned Julia users will figure out what *(a, b) is, but they will still be puzzled why you wrote it that way. Interrupting the flow of reading code for no good reason is not advisable IMO.

Well, my current assumption about the standard way to write mul, after reading this doc, is

  • The best way to write * is: figuring out a way that can bypass it totally.

something look like

julia> x = 3;

julia> 2x^2x
1458

julia> Meta.show_sexpr(:(2x^2x))
(:call, :*, 2, (:call, :^, :x, (:call, :*, 2, :x)))
julia> y, z = 4, 5;

julia> ((x)y)z
60

julia> x = [2, 3]; A = [7 8; 4 1]; y = [5, 6];

julia> x'y # == dot(x, y)
28

julia> (x'A)y # == dot(x, A, y)
244

julia> x'A
1Γ—2 adjoint(::Vector{Int64}) with eltype Int64:
 26  19

julia> (A)y
2-element Vector{Int64}:
 83
 26

While these are correct, they allocate multiple intermediate arrays and are noticeably slower than [a; c;; b; d] and [a b; c d].

I think the real moral of this story is that parsing is very fussy in space-sensitive syntax mode, which occurs inside of array concatenation and macro calls. One has to be extra careful there, especially when it comes to infix operators. Everywhere else, prefix call syntax is unproblematic.

If we were going to change anything I think we should make asymmetrical spacing around infix operators an error. With that done, this would be unambiguously parsable as a prefix call.

16 Likes

I’d say the moral of the story is that any nontrivial expression in an array instantiation needs to have parenthesis around it. As far as I’m concerned,

[(*(2, 3)) (*(3, 4));
 (*(3, 4)) (*(2, 3))]

is the one and only β€œcorrect” way of writing this, and

[(2*3) (3*4)
 (3*4) (2*3)]

is also the β€œcorrect” way of defining the matrix with an infix operator. I consider the parsing in the context of an array constructor of any not completely trivial expression as β€œundefined” and not guaranteed to be stable between Julia versions. Hopefully, this is something that linting tools like JuliaFormatter can handle automatically.

Maybe a way to define the behavior in a particularly easy and intuitive way is to just not allow spaces in an array element unless the expression is enclosed in parentheses. I don’t know if that would be considered β€œbreaking”, or merely changing β€œundefined” to β€œwell-defined”.

This is much more of a β€œdon’t hold it that way” issue than a fundamental problem, IMO.

1 Like

should we add that to the potential strict mode spec?

8 Likes

Yes, definitely. Might also be good to disallow misleading spacing where operators with lower precedence have less space around them. Eg 1+2 * 3 or a:b + 1. Also potentially mixing of operators with non-obvious precedence relationships, which I can’t think of an example of off the top of my head.

10 Likes

A related point is here: I think julia should discourage the users using infix operator across lines, when they are confronting lengthy expressions. New users may tend to write

import Random; 
a, b, c, d, e, f, g = rand(7);
Random.seed!(2);
no_good_expr_1 = (
    rand(1:9) * log(a)
    + (sin(b))e^rand(1:9)
    + abs2(g)/rand(1:9)
    + exp(f)/rand(1:9)
)
Random.seed!(2);
no_good_expr_2 = rand(1:9) * log(a) +
    (sin(b))e^rand(1:9) +
    abs2(g)/rand(1:9) +
    exp(f)/rand(1:9)

Julia should let people be aware that they can write this more clearly as

Random.seed!(2);
good_functional_expr = Base.:+(
    rand(1:9) * log(a),
    (sin(b))e^rand(1:9),
    abs2(g)/rand(1:9),
    exp(f)/rand(1:9)
)

Maybe still a better example is -, which is both β€œminus” (infix) and (prefix) β€œnegation” (-a === -1 * a).

It might also be nice to consider the β€œamount” of spacing on both sides, so the following:

julia> 1 : 3  +  1
1:4

would also be flagged. As for it being an error or a warning is also a question.

1 Like

Personally, I would never write that code like that and haven’t seen anything be else do it, so I think that’s unlikely for be a style suggestion with unanimous support. I think the first two ways to write it are both fine.

4 Likes

Why? Formulas spanning multiple lines have been used for more than centuries at this point in mathematics and the sciences. They are understood well, and work fine in code, especially if you include parentheses.

Feel free to use any notation you like, but bending Julia towards non-standard notations for math is unlikely to get any significant buy-in from others. Pretty much the whole point of Julia’s surface syntax is to get infix, otherwise we could have S-expressions and keep the parser very, very simple.

9 Likes