Syntax Surprises

Yeah, as much as syntax conflicts like this are conceptually annoying and make for great syntax “wat” examples, this just doesn’t seem like it’s actually a big issue in practice and so isn’t worth the churn or breakage to eliminate wats. Sometimes I wear more of a “what if we did this?” hat; but other times I have to put a more classic, boring hat, which defaults to leavings alone that are good enough.

6 Likes

That being said, I agree with comments above that obfuscated syntax is absolutely something that would be good to check in a linter, e.g. give a warning if the user has a variable named f0 in the same scope that uses single-precision literals like 2f0.

(One of the reasons that this doesn’t show up very often, I suspect, is that production Julia code doesn’t use single-precision literals all that often — if you want your code to handle single precision, then typically you want it to handle all precisions, and you write precision-independent literals based on the types of the function arguments.) Not really relevant since the hypothetical confusion is in the opposite direction (but requires variables named e or f and for you to write something like 2e-1 without noticing it looks like a literal).

7 Likes

I would like to express that I appreciate this. We can’t be confident we’ve found an optimal solution if we don’t occasionally entertain the possibility that we haven’t.

I very much like numeric literal juxtaposition; the improved readability reduces error rate; it’s simple, intuitive, and hard to misinterpret. Most of the WATs are only encountered if you go looking for trouble.

My concern is, I wouldn’t want to pick the language that solved the interpreted vs. compiled two-language problem, only to run into another two-language problem a few years later (small-scale scripts vs. production code). Numeric juxtaposition is hard to get wrong, except for ambiguity over e, E, and f which raises the possibility of runtime black swans; sooner or later some knucklehead on the team is bound to mess it up (probably me) and ruin the party. I’d want assurance that, on a risk-adjusted basis, I’m not paying a penalty for the feature.

A linter solves this. I don’t need it right now, but having it on the roadmap puts my unease to rest: that when the time comes for a project to scale and a bunch of under-supervised 18-year-olds start getting thrown at it, or when Elon decides to use Julia for FSD, automated checks can help steer them away from trouble.

2 Likes

@stevengj I’d also like to express gratitude for steadfastly holding your ground and forcing us to figure out how to make it work the way it is.

1 Like

Surprise - can we rely on that?

julia> foo((a,b...)...) = [a, b]
foo (generic function with 1 method)

julia> foo((1,2,3), (4,5,6), (7,8))
2-element Vector{Tuple{Any, Any, Vararg{Int64}}}:
 (1, 2, 3)
 ((4, 5, 6), (7, 8))
1 Like

Yes.

1 Like

I can’t see the example matches the manual. Over there we don’t have the double ellipsis.

Did you miss the sections about varargs functions and destructuring assignment and multiple return values?

No. I don’t get it. My example behaves like foo(a, b...) = [a, b]. There is no example or rule explaining the syntax I used.

Hah, that’s a fun one. It’s conceptually similar to having a bunch of extraneous parentheses, with this weird exception:

Function Abduction (Part Deux)

julia> f(((((x,)...,)...,)...,)...) = x+27
f (generic function with 1 method)

julia> f(15)
42

julia> f(42, "where", :do, `these`, r"go?!?")
69

It’s also handy for masking the method signature, to prevent people from knowing the argument names:

julia> methods(f)
# 1 method for generic function "f" from Main:
 [1] f(...)
     @ REPL[1]:1

Here’s another fun one. Apparently underscore _ is allowed as a fieldname for now :smiling_imp:

Under Scores Under Stress

julia> Main._ = "hi!"
"hi!"

julia> hellos = (a = "hello!", b = "hola!", _ = "hallo!")
(a = "hello!", b = "hola!", _ = "hallo!")

julia> (; b, _, a) = hellos  # destructure syntax
(a = "hello!", b = "hola!", _ = "hallo!")

julia> @show Main._
       @show a
       @show b
       @show _
Main._ = "hi!"
a = "hello!"
b = "hola!"
ERROR: syntax: all-underscore identifier used as rvalue around show.jl:1128

And another. The surprise in this one is the unprivileged treatment of a local function’s name despite using named function syntax, and it should not be possible for performance reasons, but it’s a fun brain teaser for now:

Method Impossible

julia> const f = let start=4, c=start+1, f(x)=x
           f() = if f(c-=1)==start; "Your mission, should you choose to accept it. 📦"
                 elseif c>0; "This message will self-destruct in $c times. "*"🕐🕑🕒"[4c-3]
                 else f=nothing; "Self-destructing! 🔥" end
       end;

julia> println(f())
       println(f())
       println(f())
       println(f())
       println(f())
       println(f())
Your mission, should you choose to accept it. 📦
This message will self-destruct in 3 times. 🕒
This message will self-destruct in 2 times. 🕑
This message will self-destruct in 1 times. 🕐
Self-destructing! 🔥
ERROR: MethodError: objects of type Nothing are not callable
5 Likes

With the declaration foo(x...), you get all the arguments in a tuple called x (the vararg feature described in the doc). When you write foo((a,b...)...) you effectively replace x with (a,b...). This is a destructuring assignment, of the kind described at the end of this section. You get the first value of x in a and the rest in b. (And as @uniment showed, you can also write (a,) to get the first value and discard the rest.)

Ok, that is what I read from the doc.

There is not mention of effective replacement in the docs, only ellipsis after a variable appears there.
Also foo((a,b)...) is not effectivly replaced by foo(a, b).

Yes. You can do quite a lot of things, which are not documented; and then guess what is the semantics behind that. That is not like it should be!

Here’s how I understand it (the following quotes are from the documentation):

The destructuring feature can also be used within a function argument. If a function argument name is written as a tuple (e.g. (x, y) ) instead of just a symbol, then an assignment (x, y) = argument will be inserted for you […]

On the surface this says that foo(x) can be written foo((a, b)), but it also references “the destructuring feature” which is described earlier in the page, and which includes this:

If the last symbol in the assignment list is suffixed by ... (known as slurping ), then it will be assigned a collection or lazy iterator of the remaining elements of the right-hand side iterator

followed by the example of a, b... = "hello". Since that’s part of the feature that “can also be used within a function argument”, it’s logical that you can do foo((a, b...)).

Finally, the page also says

You can define a varargs function by following the last positional argument with an ellipsis

In the declaration foo((a, b...)) the function has one positional argument: (a, b...). So making it a vararg gives foo((a, b...)...).

The documentation can surely be improved. It’s true it doesn’t say explicitly that all the features can be combined in this way, maybe that would be a worthy addition… On the other hand if you decide to nest fancy syntaxes as in the foo((a, b...)...) example, I think the hard-to-interpret problem is on you, and it’s not surprising that such a convoluted combination is not explicitly documented…

Again from the documentation:

A comma-separated list of variables (optionally wrapped in parentheses) can appear on the left side of an assignment: the value on the right side is destructured by iterating over and assigning to each variable in turn:

julia> (a,b,c) = 1:3
1:3

julia> b
2

The value on the right should be an iterator (see Iteration interface) at least as long as the number of variables on the left (any excess elements of the iterator are ignored).

The last sentence says that you can indeed write (a, ) (a comma-separated list of one element) to keep only the first element.

The syntax on the left hand side of an assignment is not identical to the syntax of the formal arguments of a function. For example (a, b...)... = 1 ,2, 3 is a syntax error. Therefore some of your conclusions seem wrong to me.

The documentation does say that in the arguments of a function, you can use the “destructuring assignment” syntax. It doesn’t say that variable assignment can use the function vararg syntax.

Actually it states: “The destructuring feature can also be used within a function argument.” The relationship between this statement and the vararg function arguments is nowhere explicitly specified.

Please let’s stop this. I am telling, that the doc is unclear to me, what is my subjective opinion. If everybody else thinks it is clear, a accept this with astonishment.

1 Like

I was today years old when I learned that you can discard the rest of the elements like this:

Destructure Discard

julia> a = Array{Int}(undef, 3);

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

julia> a
1
1 Like

In how many scenarios would this be of benefit?

The big one that comes to mind is range(start, stop, len); could it be suitably addressed with this?

julia> Base.range(start, stop, len::Float64) = range(start, stop, Int(len))

julia> range(0, 10, 2e9)
0.0:5.0000000025e-9:10.0

That doesn’t seem like a good idea. The problem is the next

julia> Base.range(start, stop, len::Float64) = range(start, stop, Int(len))

julia> range(0, 10, 1.5)
ERROR: InexactError: Int64(1.5)
Stacktrace:
 [1] Int64
   @ ./float.jl:788 [inlined]
 [2] range(start::Int64, stop::Int64, len::Float64)
   @ Main ./REPL[1]:1
 [3] top-level scope
   @ REPL[3]:1

I think this should not be treated specially, and is conflating the usual meaning of Float64.