Syntax Surprises

It’s also a question of how to build matrices, and how to separate the elements of a row within a matrix. To demonstrate using row vectors:

julia> ([1-2+3], [1 -2+3], [1 -2 +3], [1- 2+ 3])
([2], [1 1], [1 -2 3], [2])

Add a comma and it throws an error:

julia> [1 -2 +3,]
ERROR: syntax: unexpected comma in array expression

similarly,

julia> [1 -2, +3]
ERROR: syntax: missing separator in array expression

This is because commas are used only for building one-dimensional arrays, i.e. column Vectors, and that’s incompatible with matrices and other multidimensional arrays.

To do arithmetic on elements of the vector, spaces around the operator must be symmetric:

julia> [1-2, +3]
2-element Vector{Int64}:
 -1
  3

julia> [1 - 2, +3]
2-element Vector{Int64}:
 -1
  3

(more precisely, if there’s space on the left-side of the operator, then if there is no space on the right-side of the operator it will not be treated as a binary operator but instead as a unary operator.)

To build a higher-dimensional array such as a matrix, use semicolons or newlines:

julia> [1 -2 +3; -4 +5 -6]
2×3 Matrix{Int64}:
  1  -2   3
 -4   5  -6

In this expression, it becomes perfectly clear why these parsing rules work this way.

You can also build a column vector using semicolons or newlines instead of commas, because column vectors are just the tiniest subset of multi-dimensional arrays:

julia> [1; 2; 3]
3-element Vector{Int64}:
 1
 2
 3

let’s do some arithmetic:

julia> [1+1; 2+2; 3+3]
3-element Vector{Int64}:
 2
 4
 6

But add funny spaces around elements, and you can get an error:

julia> [1+1; 2 +2; 3+3]
ERROR: ArgumentError: argument count does not match specified shape (expected 3, got 4)

Julia’s parser is very clever for this specific reason—within the bounds of [], it’s assumed that expressions will be space-delimited for matrix-building, and so parsing rules needed to be made to handle this. And then, similar parsing rules are applied when we call macros without parentheses because we’re using spaces again as a separater between arguments.

It’s just something to be mindful of; you’ll run into it sooner or later anyway, either when building arrays or when calling macros, so it’s best to just adopt habits of symmetrical whitespace around operators. Symmetrical whitespace is a good habit anyway for code legibility.

Note that these considerations only apply to plus and minus, which are the only operators which can either be binary or unary; other operators don’t act like this:

julia> ([1/2*3], [1 /2*3], [1 /2 *3], [1/ 2* 3])
([1.5], [1.5], [1.5], [1.5])

Best to adopt good whitespace habits anyway.

Edit:

Note that it could have been decided, instead of having whitespace-dependent rules for when + and - would become unary operators, to simply wrap expressions in parentheses within matrices:

julia> [1 (-2) (+3)]
1×3 Matrix{Int64}:
 1  -2  3

Then there would be no room for confusion surrounding this topic, neither within arrays nor in macro calls.

However, building large matrices would become a pain. Julia’s creators, being math-centric folks who love matrices, chose to prioritize succinctness in matrix building even though it calls for some context-dependent whitespace-dependent parsing rules.

I’m partial to math myself (after all, math is the closest thing we have to a universal language), so I can’t fault them for that. But it does demonstrate quite nicely how decisions in a language design will necessarily revolve around the priorities of its authors.

Not just matrices, but anything wrapped in brackets or parentheses:

julia> if true Any[] end
Any[]

julia> if true [x for x=1:3] end
ERROR: syntax: space before "[" not allowed in "true [" at REPL[2]:1

julia> if isodd(1) (1,2,3) end
ERROR: syntax: space before "(" not allowed in "isodd(1) (" at REPL[3]:1

There’s a good chance this is a bug, but I don’t think #3 in the OP is; I suspect I might simply adopt a habit of adding a semicolon after the condition for one-liner ifs, fors, and whiles to avoid it altogether. I’ve already had to adopt a similar habit for one-liner let, do, and try...catch statements anyway.

This behavior I actually don’t find egregious. It’s enough of an edge case (who makes multidimensional arrays of functions anyway? :eyes:), and can be disambiguated with [cos (<(5))] or [cos;;<(5)]. Although of course I would prefer, instead of <(5), to use underscore partial application syntax _ < 5.

If you did want to make a multidimensional array of disparate functions though (why??), you’d probably want to roll a new type that maintains type stability by storing them in a Tuple.

I guess this is just highlighting how inconsistent mathematical notation is :sweat_smile:

In math we can write (a+b)(c+d) and it will mean implicitly (a+b)\times(c+d), yet we can also write (f * g)(t) and it does *not* mean (f * g)\times (t) (where * refers to convolution). The fact that the parsing rules of math depend on context and the type of object—which we use naming conventions and natural language descriptions to disambiguate—makes it impossible to architect a perfectly consistent programming language around it.

Yet we try, and for good enough reason: if the goal is consistency at all costs, using the language becomes difficult not for ambiguity, but for lack of resemblance with anything meaningful to a human (and everything is wrapped fifteen layers deep in parentheses). The extra verbosity to eliminate ambiguity in all edge cases reduces the SNR of the base cases, which then increases error rate. After all, isn’t resemblance to math, at least in part, what brings us to Julia?

Consistency is very valuable, but is not the sole measure of quality. The question then becomes not about how to achieve perfect consistency, but about what set of trade-offs is optimal. As we know about most real-life optimization problems (i.e., high-dimensional, nonlinear, and poorly observable; it has been said that life itself is a POMDP), there’s no knowably optimal answer; we descend gradients, we reason, we experiment, we develop heuristics, and of course we opine, but the ultimate referee is Father Time.

Languages like mathematics and English have spent hundreds of years being refined, even if imperfectly, to balance these demands—to maximize information transfer rate whilst minimizing error rate and maintaining sufficient brevity for experiments and derivations. By Lindy’s Law, these languages should serve as a decent starting point for programming language design; indeed, many successful languages have incorporated some blend of their features, Julia especially so.

Various features can be debated here and there, but 4(4) is pretty unambiguous as multiplication instead of function call so I don’t really see how it should raise too much controversy. For any weirdos who want to call their integers, I think it’s fairly agreeable that they should be forced to write x=4; x(4).

2e + 1, on the other hand, kinda scares me. Same for this:

f2=101
2(f2)  #-> 202
2f2    #-> 200.0f0

Not sure why it took so long; on my system it’s immediate.

To answer this requires a greater mind than mine :sweat_smile:

2 Likes

I don’t think oldness is the “ultimate referee” — this isn’t the best of all possible worlds. IMO, a lot of math notation is confusing and impairs reading, learning, and understanding, relative to a more consistent, composable language.

1 Like
Getting slightly off-topic here 😅

I wouldn’t argue that it is! (unless I belonged to a species occupying a much more stable niche, like a crocodile :wink:)

I’m merely expressing views about optimum finding. In partially-observable nonlinear stochastic high-order dynamical systems (aka “life”), the optimum will invariably contain features that are arguably inconsistent and irreconcilable (I think humans develop humor and cognitive dissonances to handle this without becoming self-destructive). So, it’s not the worst idea to start with what works and iterate from there, because there’s a good chance that what exists has found an optimum you wouldn’t think of or logically agree with.

Just like simulated annealing though, there’s no guarantee you’ve found the global optimum; in all likelihood we haven’t. And in all likelihood, tomorrow’s optimum won’t be the same as yesterday’s. So we keep experimenting and adapting.

To be slightly more technical, the ultimate referee is survivorship despite a barrage of challenges and competitors. Time is a proxy for experience, ceteris paribus. “Ceteris paribus” is the tricky bit.

This could make for an interesting thread!

P.S. your wiki article has a fun easter egg

For instance, it is logically possible that a meteor might have fallen from the sky onto Wikipedia founder Jimmy Wales’s head soon after he was born, killing him. But it is not logically possible that what happens in a given world (e.g. that Jimmy Wales founded Wikipedia) also does not happen in the same world (i.e. that Jimmy Wales did not found Wikipedia). While both of these events are logically possible in themselves , they are not logically possible together , or compossible – so, they cannot form part of the same possible world.

Completely agree. This is exactly what I was just about to reply to @uniment.

I wonder whether programmers really find the ability to premultiply by a numeric literal like 4a + 5b or like (5)(5) useful enough. Does it really improve coding speed that much? Does it help understand code better than the usual notation 4 * a + 5 * b?

Perhaps, but what’s the cost of such resemblance to math? If it’s confusing syntax like in OP or complicated and error-prone context-sensitive parsing, then I’d rather write 4 * a instead of 4(a).

IMO, it’s pretty unambiguous as a function call, because basically all widely used languages treat <thing>(<thing>) as a function call:

  • R, the language of statisticians, doesn’t let you call your numbers.
  • Python, the language of data scientists, doesn’t let you call numbers either.
  • The Wolfram language, arguably the language of mathematicians, does support this syntax:
    In[1]:= 4(6)
    Out[1]= 24
    
    However, Wolfram uses square brackets [] instead of parentheses () for function calls, so 4(6) can’t be interpreted as a function call and is thus unambiguously multiplication. A function call looks like Length[{2,3,4}] == 3.
  • C and Rust, systems programming languages, definitely don’t allow this. Rust’s error message clearly says “call expression requires function” about code like 4(5).

…which is yet another confusing consequence of the same design choice to allow multiplication by calling numbers or prepending numeric literals to identifiers.

The ultimate questions are: do Julia programmers find this syntax useful enough? How many of us prefer it to the usual 4 * a? What are its advantages compared to the usual syntax, except being at most 3 characters shorter? What are its disadvantages? Do the advantages outweigh the disadvantages?


Petition to make syntax like 4a, 4(a) and (4)(a) an error in Julia 2.0.

I think you’ll find that a lot of us like this syntax — it’s less about saving one * character and more that 3x+1 is more readable than 3*x+1 — and it doesn’t seem to have been a practical source of lots of bugs.

This seems like one of those things that is simply not on the table: PSA: Julia is not at that stage of development anymore

13 Likes

I like this syntax as well. I also think that 3x+1 is more readable than 3*x+1.

But when I discovered that this allows “calling numeric literals” like 3(x) + 1, then noooooooo, I don’t like this syntax enough to tolerate this kind of weirdness. It’s not worth it, IMO.

Also, if 4(6) is multiplication, then 4.(6) should be multiplication too! Julia disagrees:

julia> 4.([6,7])
ERROR: syntax: numeric constant "4." cannot be implicitly multiplied because it ends with "."
Stacktrace:
 [1] top-level scope
   @ none:1

The parser understands that 4. is a “numeric constant”, but refuses to multiply because “it ends with .”?

  • This error message doesn’t tell me why it can’t be implicitly multiplied. “it ends with .” - why is this a problem?
  • It seems like here the multiplication syntax collides with the “map”-style function calls (like sqrt.([1,2])), so Julia itself can’t quite decide whether to interpret this as multiplication or a function call. However, it’s clearly multiplication, since the first expression is a “numeric constant 4.” and prepending numeric literals like this means multiplication.

4.0([6,7]) is interpreted as multiplication without issues.

We could certainly have a better error message here — feel free to submit a PR. (See also julia#22498 — this error-message code needs updating anyway.)

The original choice to make this an error was in julia#16339 (though the message was later revised) based on the discussion in julia#15731; as I recall, the feeling was that allowing 4.x for 4.0*x is just too confusing, especially in conjunction with dot operators (and later dot calls) and other styles of numeric literals.

5 Likes

I would suggest:

…numeric constant 4. cannot be implicitly multiplied because it ends with ..
Syntax like 4.xyz or 4.(xyz) can be confused with dot calls my_function.(value) and field access my_struct.field, so it was excluded from the implicit multiplication syntax.

This is what all the GitHub issues seem to be saying too. However, it just further highlights that implicit multiplication syntax is confusing. Not sure if that’s the kind of error message developers of Julia want to have in the language. On the other hand, it’s an honest error message which admits that some syntax is confusing…

1 Like

I always maintained that that was a mistake. Obvious code is good code. And vice versa.

3 Likes

I wanted to see if how JuliaSyntax.jl parsed these statements.

julia> using JuliaSyntax: JuliaSyntax, SyntaxNode, GreenNode

julia> JuliaSyntax.parse(SyntaxNode, "@show 1-2+3")
((toplevel (macrocall @show (call-i (call-i 1 - 2) + 3))), JuliaSyntax.Diagnostic[], 12)

julia> JuliaSyntax.parse(SyntaxNode, "@show 1 -2+3")
((toplevel (macrocall @show 1 (call-i -2 + 3))), JuliaSyntax.Diagnostic[], 13)

julia> JuliaSyntax.parse(SyntaxNode, "@show 1 -2 +3")
((toplevel (macrocall @show 1 -2 3)), JuliaSyntax.Diagnostic[], 14)

julia> JuliaSyntax.parse(SyntaxNode, "@show 1- 2+ 3")
((toplevel (macrocall @show (call-i (call-i 1 - 2) + 3))), JuliaSyntax.Diagnostic[], 14)

julia> JuliaSyntax.parse(SyntaxNode, "[<(5) cos]")
((toplevel (hcat (call < 5) cos)), JuliaSyntax.Diagnostic[], 11)

julia> JuliaSyntax.parse(SyntaxNode, "[cos <(5)]")
((toplevel (vect (call-i cos < 5))), JuliaSyntax.Diagnostic[], 11)

julia> JuliaSyntax.parse(SyntaxNode, "2 > 1 ? :Hello : :World")
((toplevel (? (call-i 2 > 1) (quote Hello) (quote World))), JuliaSyntax.Diagnostic[], 24)

julia> JuliaSyntax.parse(SyntaxNode, "if 2 > 1  s=:Hello  else  s=:World  end")
((toplevel (if (call-i 2 > 1) (block (= s (quote Hello))) (block (= s (quote World))))), JuliaSyntax.Diagnostic[], 40)

julia> JuliaSyntax.parse(SyntaxNode, raw"s = if 2 > 1  :Hello  else  :World  end")
((toplevel (= s (if (call-i 2 > (call-i 1 : Hello)) (block) (block (quote World))))), JuliaSyntax.Diagnostic[], 40)

julia> JuliaSyntax.parse(SyntaxNode, "f() = (local a, b = 1, 2; a+b)")
((toplevel (= (call f) (block (local (= (tuple a b) (tuple 1 2))) (call-i a + b)))), JuliaSyntax.Diagnostic[], 31)

julia> JuliaSyntax.parse(SyntaxNode, "f() = (a, b = 1, 2; a+b)")
((toplevel (= (call f) (tuple a (= b 1) 2 (parameters (call-i a + b))))), JuliaSyntax.Diagnostic[], 25)

julia> JuliaSyntax.parse(SyntaxNode, raw"""for x=1:3  print("$x ")  end""")
((toplevel (for (= x (call-i 1 : 3)) (block (call print (string x " "))))), JuliaSyntax.Diagnostic[], 29)

julia> JuliaSyntax.parse(SyntaxNode, raw"""for x=(1,2,3)  print("$x ")  end""")
((toplevel (for (= x (tuple 1 2 3)) (block (call print (string x " "))))), JuliaSyntax.Diagnostic[], 33)

julia> JuliaSyntax.parse(SyntaxNode, raw"""for x=1:3  (print("$x "))  end""")
((toplevel (for (= x (call-i 1 : 3)) (block (call print (string x " "))))), JuliaSyntax.Diagnostic[], 31)

julia> JuliaSyntax.parse(SyntaxNode, raw"""for x=(1,2,3)  (print("$x "))  end""")
((toplevel (for (= x (call (tuple 1 2 3) (error-t) (call print (string x " ")))) (block))), JuliaSyntax.Diagnostic[JuliaSyntax.Diagnostic(14, 15, :error, "whitespace is not allowed here")], 35)

julia> JuliaSyntax.parse(SyntaxNode, raw"(4)(4)")
((toplevel (call-i 4 * 4)), JuliaSyntax.Diagnostic[], 7)

julia> JuliaSyntax.parse(SyntaxNode, raw"(4)(2*2)")
((toplevel (call-i 4 * (call-i 2 * 2))), JuliaSyntax.Diagnostic[], 9)

julia> JuliaSyntax.parse(SyntaxNode, raw"(2*2)(4)")
((toplevel (call (call-i 2 * 2) 4)), JuliaSyntax.Diagnostic[], 9)

julia> JuliaSyntax.parse(SyntaxNode, raw"+(1, 2, 3)")
((toplevel (call + 1 2 3)), JuliaSyntax.Diagnostic[], 11)

julia> JuliaSyntax.parse(SyntaxNode, raw"-(1, 2, 3)")
((toplevel (call - 1 2 3)), JuliaSyntax.Diagnostic[], 11)

julia> JuliaSyntax.parse(SyntaxNode, raw"if 1 > 2  A=[1 2; 3 4]  else A=[4 3; 2 1] end")
((toplevel (if (call-i 1 > 2) (block (= A (vcat (row 1 2) (row 3 4)))) (block (= A (vcat (row 4 3) (row 2 1)))))), JuliaSyntax.Diagnostic[], 46)


julia> JuliaSyntax.parse(SyntaxNode, raw"A = if 1 > 2  [1 2; 3 4]  else [4 3; 2 1] end")
((toplevel (= A (if (call-i 1 > (typed_vcat 2 (error-t) (row 1 2) (row 3 4))) (block) (block (vcat (row 4 3) (row 2 1)))))), JuliaSyntax.Diagnostic[JuliaSyntax.Diagnostic(13, 14, :error, "whitespace is not allowed here")], 46)
1 Like

I absolutely do find this notation useful and attractive. It’s certainly not about coding speed at all, but about readability. I especially prefer

(2x + 4)/3a

over

(2*x + 4)/(3*a) 

The more complicated the expression, the greater the utility of literal coefficients and unicode identifiers.

Would anyone ever write 4(a) or 4(4), except as an experiment? Allowing or disallowing it wouldn’t bother me, no one would use it anyway.

8 Likes

I think the meaning of 4a is obvious.

4 Likes

From an a to an e is just a few keys on the keyboard. And it isn’t just e:

julia> f = 2
2

julia> 3f-3
3.00000e-03

julia> 3f - 3
3
6 Likes

Another little inconsistency:

julia> e,E,f,F = 3,3,3,3
(3, 3, 3, 3)

julia> 3e-3, 3f-3, 3E-3, 3F-3
(0.003, 0.003f0, 0.003, 6)

'e' is case insensitive, while 'f' is case sensitive.
(aside: I like 2a == 2*a , but Float32 literals can change)

12 Likes

Why not 2(x+2)/3a?

This is kinda cursed ngl

Interestingly, the Discourse syntax highlighting [incorrectly] shows 3F-3 as a numeric literal, but my VSCode syntax highlighting [correctly] doesn’t.

(also, I should pick different colors in VSCode; the color of numeric literals is too close for comfort to variables.)

1 Like

To be wild here, what if we did away with 2e9 notation instead? It’s not a great syntax. It’s not obvious at all and people often expect it to produce an integer. You could make 2×10^9 a literal syntax instead, which is much more obvious and could produce an integer. If we disallowed 2e9 and required 2.0e9 for the float literal and also disallowed float literal juxtaposition then there wouldn’t be any more syntax ambiguities.

14 Likes

Since you are a newcomer here, I should probably point you to this post: PSA: Julia is not at that stage of development anymore

62 Likes

:smile: I mean as a 2.0 thing, of course. But yeah, maybe too disruptive. Of course literal syntax changes are the easiest thing to automatically update. We could also have syntax changes be module-locally opt-in pre 2.0 and opt-out post 2.0.

12 Likes