Inconsistencies in Julia syntax and semantics

I’m not sure where is right for an average user to put their thoughts, but I’ll give it a try here.

I’m interested in some inconsistencies (as I see them) in Julia that I can only guess arose from its developmental roots (MATLAB as one influence?):

  • Using size instead of shape as a complement to reshape
  • Dot notation before for operators and after for functions (.+ vs sin.(…))
  • Range indexing returning a copy but passing arrays into functions resulting in a view

These inconsistencies are small – they have little to do with functionality – but I see reconciling them as a way to improve the language. There may be others too. In general, I would be interested in removing any inconsistencies in the core language where possible. Making fundamental details consistent within the language leads to more ease in using the language, especially for those starting out. I value this higher than similarity to other languages or maintaining long traditions. Understandable if others have different priorities. It’s definitely something that would wait for a version jump from 1 to 2 since the initial effect on the entire ecosystem would be significant.

passing array into a function is not slicing, it’s just pass-by-sharing.

length vs. resize? it’s just some wording, shape is not occupied I guess technically we can have const shape = size.

this is to eliminate ambiguity, if you use post-fix instead of in-fix, it’s consistent

julia> (+).([1,2], [3,4])
2-element Vector{Int64}:
 4
 6

`size`, `reshape` not consistent · Issue #22665 · JuliaLang/julia · GitHub. Also this is not a syntax.

See full discussion here. Alternative syntax for `map(func, x)` · Issue #8450 · JuliaLang/julia · GitHub. There are debate over .sin vs sin.. TL;DR is roughly that .sin would be a version of sin that’s broadcasting which is not the current meaning of the syntax. However, with sin.() and with the follow up syntax change, the .() becomes the broadcasting call operator and it is consistent with other broadcasting operators. Essentially we picked this syntax since the meaning of it is more useful.

Passing array into functions does not result in a view, it’s literally passing the same object. The “inconsistency” you are talking about is basically that b = a is the same object whereas b = a[:] is a copy, or that a[:] is not the same as a. That’s not really a trivial consistency to require and there has been talk about whether a[:] should be view by default. So far though, it seems that copy by default and view with @view is a good enough compromise. (Also, this is not syntax, this is semantics but I digress)

4 Likes

I don’t see. Could you explain what ambiguity it eliminates? I’m seeing the relationship between operators and functions as part of the motivation for consistency in how the dot syntax is used.

@yuyichao Thanks for the links. It’s good to see these things have been discussed before.

I had never thought that .() would be considered an operator. I saw the parallel with operators just being functions with extra allowed syntax, e.g. +(1, 2) being the same as 1 + 2. In fact, it still doesn’t make full sense to me. To broadcast + one has to use .+([1,2], [3,4]), while +.([1,2], [3,4]) gives an error. I didn’t find exactly why sin.(...) had to be used instead of .sin(...) in those threads. Are you saying the fusing broadcasting was somehow impossible with the latter syntax?

Yes, I’m talking about the whole combination of how Julia appears to and is used by the user – syntax, naming, semantics. (I would need to change the discussion title, but don’t have the option.) The range indexing did need more explanation. I didn’t mean that both operations had a one-to-one relationship. Rather, I was pointing out how mutable objects, like arrays, should be handled consistently by the language: if a copy is desired, then an explicit call to copy should be made, otherwise a view is used. When an array is passed into a function, it is received by the function as a reference to the same object in memory, not a copy. When an element within an array is indexed that element itself is accessed, not a copy. I would expect then that when I index a sequence of elements within an array, those same elements in memory would be accessed, not a copy. And for any of these cases if I want a copy, I should do it explicitly.

As an addendum about performance (since that is a main objective in Julia), returning a view into an array always uses less memory and sometimes uses less time (depending on the access pattern). Returning a copy always uses more memory and sometimes uses less time. So defaulting to returning a view would be a win and a tie instead of a loss and a tie.

These two cases actually perform equivalently wrt copying elements. With both b = a[123] and b = a[123:456], one cannot use b to set any element in a: they are both semantically copies of the element(s) in the original array, if they are immutable themselves. For elements that are mutable, they can of course be mutated through b, and those changes would be visible through a - again, same in both cases.

2 Likes

I have also wished occasionally that operators were dotted like this: x +. y, I think it would be easier to remember. I do think that putting the dot after the function was the right call, but it should perhaps have been like that for operators too.

IMHO this example actually makes it worse, not better :grimacing: But, yes, it is indeed consistent for function call syntax.

I also wish slices made views, but that’s not actually an inconsistency.

Well I support this default choice as it’s safer (and you have the faster option). E.g. if you would return a view to an array from a function, and share memory, even to other functions, and overwrite each others memory, arbitrarily down the line, it seems like a recipe for disaster.

It reminded me of Rich Hickey’s excellent keynote (my favorite programming talk), i.e. that is an instance of what he calls place-oriented programming (as opposed to “information technology”):

What seems possible, is an optimization, that Julia’s compiler would “add” (invisible) @view where it can proof the end-result the same. At some cost to compile time… Maybe this is only likely to work with-in functions, and I haven’t checked, I doubt it’s done already. Also some tools could help (linters, or not the right tool?) by suggesting this, then you would make it explicit in your code, with no compilation-time cost.

1 Like