the langage offers very little guarantees to the programmer and does not promote proper software architecture.
As a program goes bigger and bigger in size, being able to look at just a couple functions at most to solve a problem while being sure it won’t adversely affect unrelated pieces of the program is important. If you only count on unit testing for that, your guarantee is only as good as your tests, unfortunately.
Look at go, look at Rust and for most design decisions, just ask yourself “why didn’t they go the Julia way” and you’ll probably start to see how the trade-offs differ and how Julia went for “ease of use for script” while the others went for “ease of developing big things”.
Ha! I think you’re hitting the nail on the head here, especially this: “As a program goes bigger and bigger in size, being able to look at just a couple functions at most to solve a problem while being sure it won’t adversely affect unrelated pieces of the program is important.”
Julia is my favourite language, but I share the feeling that Julia code is more brittle than say, Go code for an equal amount of effort and discipline. I think it’s a perspective I acquired after working a few years as software engineer… Here are a few concrete points:
I used to think it nice when a clever line of code does exactly the right thing. Now for industrial-grade code I prefer several lines of boring code, maybe even with a little repetition (making junior developers go ). Boring is not fun but it’s easier to read and more robust. Julia however is so expressive… it’s a joy to write, but the code can be tricky to understand.
For example it has outstanding support for metaprogramming. Well, real-life experience: I wanted to tune the behavior of the Markdown standard library, to allow writing $ x = 2 $
instead of $x=2$
. Maybe I found the relevant code here but I wasn’t sure. I looked at the macro definition and the rest of the file, and decided to stop procrastinating and go back to work. In Go there is no meta-programming. That code would look like this and this which is sooo pedestrian, but that’s a good thing! (from a software engineering perspective).
Another point: interfaces are a core concept in software engineering, but they are not formally supported in Julia:
- The most superficial (but real) problem is duck-typing. Does your type actually implement the interface it’s supposed to? In Go you get immediately a compile error if you forgot to implement a method, or made a mistake in a method parameter. In a large project, I can add a method to an interface and immediately find all places that must be fixed. In Julia I must run tests (much slower) and pray that people actually wrote tests for all uses of the interface.
- A bit more subtle: In Go, you define interfaces explicitly and this can serve as reference for people that need to implement them. In Julia there is nothing forcing you to document your interfaces. The “interface” is whatever list of methods your code happens to use at a particular time. Real-life example: I could not figure out how to implement the IO interface to solve a simple problem (counting bytes written to a stream).
- Also, the language doesn’t help you dispatch based on interfaces implemented by a type (Holy traits are a kludgy workaround and don’t help with existing code). Even if you’re lucky and the interface is one of the few documented, there’s no mechanism to dispatch on it. Real-life example: I wanted to implement a fallback
last
method for iterators. The interface is documented, but I can’t define last(::Iterator)
. I could not find a backward-compatible solution that dispatches without runtime penalty so I gave up.
The overall point here is that Julia has weak support for one of the most important tools to make robust software.
Last point: local definitions. In Go, you can’t include files: you can import packages and one package = one directory and each file has an explicit list of imported packages at the top, and importing names in the main namespace (like using
in Julia) is strongly discouraged. In Julia, files can include files can include files… Open a file, you don’t know in which context it is included so you can’t be sure how it’s going to be interpreted. Maybe it’s even included several times in different contexts? Maybe the behavior depends on the order of includes?
Another way to put it: declarative (rather than imperative) code is generally considered more robust or at least easier to analyze and verify. In Go, you write imperative code in functions, but the overall organization (definition of packages and functions) is declarative. In Julia, this organization itself is imperative.
(That being said, maybe these Julia “downsides” are necessary for what makes it unique and wonderful, such as the interoperability of packages? I’d rather take Julia as it is, than have another Go or whatever).