Julia - is a fractal of good design?

Hello, dear developers of the Julia language!

I came across one very popular article about the PHP language, namely its bad design.

PHP: a fractal of bad design

It is clear that Julia has nothing to do with PHP, but I propose to read the article and check whether there are any similar problems and defects in the language Julia. Soon the release of the language will take place and it is better to correct everything now. After the release, nothing can be changed without breaking backward compatibility.

Personally, I have high hopes for Julia language and I want to replace R with Julia.

Although Julia has some warts (many of which are being removed in a mad rush :grinning: before v1.0), I do feel it is one of the better designed languages I’ve come across (and in 44+ years of programming, I’ve come across quite a few!). (My other favorites are the version of Scheme I used back in 1980, and CLU).

Very little of that article is relevant to Julia, IMO, except maybe for the issue of debugging, which is being worked on also - it’s just a matter of time and enough skilled manpower (since cloning Keno a few times is not possible with today’s technology :nerd_face:).

3 Likes

Since this appears to be the “what design decisions do you consider questionable”-thread, I’d guess we all list what we are unhappy with (no need to extensively reply / justify, I don’t want to start a flame war; some might be matters of taste, some might just be me being stupid, some might actually be valid points)

Target audience:

PHP was originally designed explicitly for non-programmers (and, reading between the lines, non-programs); …

Typing:

It feels like julia can’t make up it’s mind about strictness of typing. Example: It is not entirely obvious what container type functions like collect or map will choose to use; or, in other words, the array type is viewed as a performance optimization, not a semantic distinction that needs to be always predictable to the programmer. This is reflected by isequal ignoring the array type. On the other hand, performant code often needs the programmer to know a lot about the types at hand. This is a conflict, of whether people want to quickly hack out some code that does something (charitable: python-style; uncharitably: PHP-style) and people who view julia as a more productive way of writing low-level code.

I often wished julia was stricter about typing. I wished isequal would consider types, such that “Any[1]!= Int[1]”. I wished UInt8(1) != Float32(1). I wished zero(AbstractFloat) was an error, such that the following code throws on inhomogenous arrays instead of producing unpredictable results depending on how map / inference decided to create the outer array:

function ms(V)
   s = zero(eltype(V))
   for v in V
       s+=v
   end
   s
end

A=AbstractFloat[Float32(0.1), Float32(0.3)]
B=Float32[0.1,0.3]
A==B
#true
ms(A)==ms(B)
#false

The problem here is that zero(Number) is not a neutral element under addition modulo === (I think it is neutral modulo isequal) and addition is not well-defined modulo isequal. Hence the result of ms(collect(some_iterator)) depends more or less on the epistemic state of the compiler about the output of some_iterator (as far as I understood).

Feature request: recursive type-checking variant of isequal (e.g. isequal_typed as a recursive version of typeof(x)==typeof(y) && isequal(x,y)). Currently type-checked equality is not just not the default, it is not provided.

Possibly a command-line flag for stricter typing rules; I think there is an ideal of well-typed code that base and most packages should strive for. A command-line flag could make all unclear things errors; this could be used for testing and development (and maybe in production for non-throwaway code).

On the other hand, sometimes I want to use julia as a scripting language (python-like dev-speed, python-like run-speed; but no language barrier to cross when calling into “good” julia code). In these cases I am incredibly thankful for julia “just doing something”. This is uncomfortably close to the linked

PHP is built to keep chugging along at all costs. When faced with either doing something nonsensical or aborting with an error, it will do something nonsensical.

The variant of using command-line flags for stricter typing rules smells of terrible kludges like “use strict”.

Sorry for the wall of text without really constructive criticism; language design is hard and full of trade-offs, and I am in total quite happy about julia.

And split off into a second reply:

Covariance of tuple types is cool. The fact that dispatch always goes to a concrete type is cool.

Sometimes (rarely) people want something else, e.g. https://discourse.julialang.org/t/missing-data-and-namedtuple-compatibility/8136/6. Something that would be cool for this are invariant tuples. An Ituple{Union{Int,Missing}, Union{Int,Missing}}would be a concrete type to dispatch on; basically Ituple{A,B} would by syntactic sugar for struct Itup_23456 v1::A v2::B end. This fights the exponential explosion of needed specializations: Pass via Ituples; then your function becomes slower, because it needs all kind of type-dependent branches; but, on the other hand, dispatch into this function, and the amount of codegen becomes faster. As far as I understood the related discussion, named tuples can currently be abused for this. But I feel that this deserves to be a first class feature, and I feel like named tuples want to become covariant in the long run. I apologize if I grossly misunderstood the current state of things, or if I failed at RTFM (e.g. maybe @nospecialize can be be made to do all this, easily).

We’ve considered invariant tuples both as an implicit optimization (i.e. if the compiler can only figure out something is Tuple{Union{Int, Missing}, Union{Int, Missing}}, basically codegen that as an invariant tuple with appropriate smoke and mirrors to make everything that looks at the type think it’s dealing with the covariant version (the key being that most things will not look at the type of the tuple after inference), and as an explicit language feature (perhaps as a natural consequence of allowing types with varargs fields in general), but there is already enough in 1.0 ;).

It will use the typejoin over all element types, except for the empty case. This is well defined and independent of the compiler. The only case where it relies on inference is for the empty array case. It used to be that it used inference for everything, but that turned out to be often confusing as you noted, since it depends on what the compiler is or is not able to deduce. I’m not entirely happy about using inference for the empty case, but it does generally do what people want in simple cases and can help with type stability (e.g. it’s kinda nice that map(identity, Int) is of type Vector{Int} rather than Vector{Bottom}).

2 Likes

Fair enough. I’ll just register the opinion “explicit is better than implicit” :wink:

Ok, that’s very predictable. Somehow I was under the misapprehension that it used some “more clever” (read: harder to predict but better in most examples) strategy for avoiding too much copying when encountering a new type. Thanks!

Nevertheless, my uncomfortable feeling with the typing/widening remains (e.g. zero not being neutral); I often feel like julia is offering to do something semi-sensible when I instead want it to throw and tell me to improve my code.

That code doesn’t involve map nor inference. The return type depends on how promotion was defined for the types involved in the computation. As @Keno noted, inference is never exposed to the user, except when calling map on an empty collection.