Hacker News new | ask | show | jobs
by mruts 2540 days ago
Julia is everything python could have been, and much more. I'm stuck with python right now as a lot of people in the data science/ML community are, but it's becoming increasingly viable to use Julia for "real" work. The Python-Julia interop story is pretty strong as well, which allows you to (somewhat) easily convert pandas/pytorch/sklearn code into Julia using Python wrappers. Julia has some unconventional things in it but they are all growing on me:

1. Indices by default start with 1. This honestly makes a ton of sense and off by one errors are less likely to happen. You have nice symmetry between the length of a collection and the last element, and in general just have to do less "+ 1" or "- 1" things in your code.

2. Native syntax for creation of matrices. Nicer and easier to use than ndarray in Python.

3. Easy one-line mathematical function definitions: f(x) = 2*x. Also being able to omit the multiplication sign (f(x) = 2x) is super nice and makes things more readable.

4. Real and powerful macros ala lisp.

5. Optional static typing. Sometimes when doing data science work static typing can get in your way (more so than for other kinds of programs), but it's useful to use most of the time.

6. A simple and easy to understand polymorphism system. Might not be structured enough for big programs, but more than suitable for Julia's niche.

Really the only thing I don't like about the language is the begin/end block syntax, but I've mentioned that before on HN and don't need to get into it again.

4 comments

I can't believe I'm jumping into the inevitable 1-based indexing discussion, but I'm surprised to see you say that one-based indexing results in "less "+ 1" or "- 1" things in your code". Most arguments I've seen come out to "it's fine" (certainly) or "it's more comfortable for mathematicians" (which I can't speak to).

Besides Dijkstra's classic paper[1] showing why 0-based indexing is superior, in practice I find myself grateful for 0-based indexing in Python because of how slices and things just work out without needing +1/-1.

I'd like to understand. Could you give an example of when 1-based indexing works out better than 0-based?

[1] http://www.cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF

The classic example is getting the last element of an array. With 1-based indexing the length of the array is the index of the last element. It has a nice symmetry to it.

Also I find it elegant that for 1-indexing that the start and end value for slices are both inclusive, instead of the first one being inclusive and the last being exclusive.

Also, isn’t it just weird that the index of an element is one less than it’s “standard” index? Like if I take the first nth elements of a list, it would stand to reason that the nth element should be the last element, right?

The reason for zero indexing is historical, related to pointer offsets. I don’t think anyone chose them to be easier for people. They just made them that way because it maps closer to how contiguous values in arrays are accessed.

Also, with 1-indexing I can multiply numbers by arrays and get reasonable offsets. 3 x 1 is three, so I would get the third element of the list. But with 0-indexing, I have 0 x 3 which gives me the same element, clearly inconsistent.

There are some good reasons for 0-indexing and I have been using it in every language for my entire career. The amount of code I’ve written in Julia is marginal compared to my 0-indexing experience, so I might be missing something.

One nice this about 0-indexing is that I can slice a list in half with the same midpoint. For example a Python array with 10 elements:

fst, snd = arr[0:5], arr[5:10]

A little nicer than:

fst, snd = arr[1:5], arr[6:10]

Though you could have inclusive slices with 0-indexing, but it would be inconvenient and suffer from the same problem as 1-indexing.

> The classic example is getting the last element of an array.

Good point, in Python I don't notice that the last element is arr[len(arr)-1] because Python provides arr[-1]. I think in general your point is that it's natural for the nth element to be arr[n].

> The reason for zero indexing is historical, related to pointer offsets.

There is that, but Dijkstra's paper makes the case from first-principles that the closed, open interval of [0,n) for sequences is the most appropriate.

> with 1-indexing I can multiply numbers by arrays... 3 x 1 is three...

Sorry, I don't understand this. It makes sense that the point I don't understand is probably most-related to why Julia chose its indexing scheme and why Matlab et al. do the same.

> One nice this about 0-indexing is that I can slice a list in half with the same midpoint.

Yeah, arr[0:index] + arr[index:len(arr)] is the full list. And to your point earlier ("if I take the first nth elements of a list"), len(arr[:n]) == n seems natural.

Edit: I've been trying to formalize why Python's indexing scheme, along with its negative-indexing, is optimal (slight pseudocode):

    l = ['a','b','c']
    n = len(l)
    i = -n
    while i < n:
        print(l[i++])
prints "a b c a b c". That code makes no reference to any bound but 'n', nor any constants (1,0) or offsets, yet it iterates over the list twice through its range (first negative indices then positive).
Dijkstra's write-up is full of subjective aesthetic judgements that certain things are ugly. I personally don't find `1:0` for an empty sequence to be ugly, and I do find using `1:1` to refer to an empty sequence and `1:2` to refer to the sequence `{1}` to be ugly. I would encourage everyone to read over his reasoning and see if you agree with his aesthetic judgements.
I’m constantly baffled by the way people hold that paper up as some sort of objective proof that 0 based indexing is superior.
Zero based indexing objectively has various convenient properties which one-based indexing doesn't.

The value of convenience over inconvenience isn't objectively better, that's all.

Objectively speaking, if I find the least positive residue of some integer modulo M, I get a value from 0 to M-1. If my M-sized array is from 0 to M-1, that is objectively convenient:

  hash_table[hash(string) % table_size]
Objectively speaking, if I have some files in a directory and I give them zero based names like file000, file001, ... then I can objectively refer to the first volume of ten using the single pattern file00?, and the next volume as file01?. If they are numbered from 001, I need file00? file010 to match the first ten. For the next ten, the file01? pattern unfortunately matches file010 so I objectively need some way to exclude it.

Objectively speaking, if I have zero based indices to a 3D array, I can find the address of an element <i, j, k> using the homogeneous Ai + Bj + Ck rather than Ai + Bj + Ck + D, which objectively adds an extra term.

Objectively speaking, the zero-based byte index B can be converted to a four byte word index using B / 4 (truncating division), and within that word, the zero-based byte local offset is B % 4. Objectively speaking, the same conversion from 1-based bytes to 1-based words requires (B-1)/4+1 and (B-1)%4+1, which is objectively more syntax and more nodes in the abstract syntax tree.

There is no reason I should like shorter, simpler, faster; that's a purely subjective aesthetic. After all, a short poem isn't better than a long one; a bacterium isn't better than a rhinoceros; and so on.

Hey, how about those one-based music intervals? C to E is a major third and all that? We have a diatonic scale with seven notes, right? As we ascend, whenever we cycle through seven notes, we have passed ... one more octave. And to invert an interval, we subtract from ... why, nine of course! And a fifth stacked on top of a fifth is a major ninth. There is objectively more cruft in 1 based music theory than 0 based. But there is no accounting for people liking it that way, right?

I think people get tricked by his way of building an argument. He structures it rhetorically almost like it is a mathematical proof, but if you read carefully the central argument is that it is "nicer"...in the particular example he picked. But if you don't pay attention it might feel like he "proves" that 0-based is universally better.

Then he goes on weird diatribes like the one about "corporate religion". I'm not sure, but I think he is trying to say that people who question his dubious logic are "sheeple".

I wasn't arguing so much that it was "objective proof", but that in contrast to "The reason for zero indexing is historical, related to pointer offsets. I don’t think anyone chose them to be easier for people.", I was pointing out that there are arguments for it as well. It's not just because "the address of an array is also the address of its first element in C".
Because of the author. If it had been someone else no one would use it as such.
> Dijkstra's paper makes the case from first-principles that the closed, open interval of [0,n) for sequences is the most appropriate

He argues that it is the most appropriate when indexing into an array of the natural numbers starting with 0. If the array in question started with 1, one-based indexing would be most appropriate following exactly the same logic!

I don't think that's a correct reading. You're talking about this section, correct?

> When dealing with a sequence of length N, the elements of which we wish to distinguish by subscript, the next vexing question is what subscript value to assign to its starting element. Adhering to convention a) yields, when starting with subscript 1, the subscript range 1 ≤ i < N+1; starting with 0, however, gives the nicer range 0 ≤ i < N.

He never specifies the contents of the array, he's talking about subscripts (i.e. indexes).

You may be right, but in that case, why is the second range "nicer"? AFACT it is because he explicitly adds 1 in the first example but implicitly subtract 1 in the second, which makes it looks cleaner. If you want the N'th element of an array, the range in the first example is N ≤ i < N+1 while in the second it is N-1 ≤ i < N. Written out like that, I don't see how the second is obviously nicer.
> Also, with 1-indexing I can multiply numbers by arrays and get reasonable offsets. 3 x 1 is three, so I would get the third element of the list. But with 0-indexing, I have 0 x 3 which gives me the same element, clearly inconsistent.

This is interesting. Suppose the task is to use this approach (index * stride) to pick every third item from a list of 9 items: [1, 2, 3, 4, 5, 6, 7, 8, 9].

With 1-indexing: Multiply the sequence of valid indices (1, 2, 3, ...) by the stride (3) and use the result to 1-index into the given list. Returns [3, 6, 9].

With 0-indexing: Multiply the sequence of valid indices (0, 1, 2, ...) by the stride (3) and use the result to 0-index into the given list. Returns [1, 4, 7].

0-indexing has the start point of the return values fixed to the origin. 1-indexing has its start point float around depending on the stride. Both work, but have different emergent properties in the given example.

It’s true that they both work, but I think the floating nature of the 1-indexing has some very desirable properties. If we wanted to sample from a distribution, the stride would relate to 1/n x 100 percent sampling. With 0-indexing we are always going to sample the first element. Granted this isn’t the best way to sample in the first place, so maybe it’s a contrived.
Shouldn’t Dijkstra’s paper be your 0th reference?
^ This is why I love hackernews!
Looking at teaching materials, indexing in R hardly gets a mention. Student are told they get get the first element by a[1] and the twelfths element by a[12], and if you want 4th, 5th and 6th, you just ask for that range, a[4:6]

For python teaching this is almost a whole chapter, with people sharing cheat sheets and building graphics to show how slicing works what not. You don't see these things in R teaching materials.

I'm sure that for the implementation of algorithms, things might be easier with zero indexing, but for a user asking for element 4,5 and 6, 1-indexing is much, much easier on the user.

In C++ you typically access arrays with unsigned integers (size_t), and a common pitfall is:

    for (size_t i = v.size() - 1; i >= 0; --i) {
      std::cout << i << ": " << v[i] << std::endl;
To fix the infinite loop you could write:

    for (size_t i = v.size(); i > 0; --i) {
      std::cout << i - 1 << ": " << v[i - 1] << std::endl;
Neither is great. Switching to signed integers might make your compiler throw warnings at you.

However, 1-based indexing does not work out well with modular arithmetic:

    # 1 based
    v[1 + (i - 1) % v.size()]

    # 0 based
    v[i % v.size()]
There's pros and cons with both schemes.
Yes, and in both cases the language should provide tools so you don't have to deal directly with those edge cases. For example in Julia, for modular arithmetic with 1-based indexing there is mod1 [1], and for iterating in Julia you should use eachindex which will always work for both 0 or 1 indexed arrays.

[1] https://docs.julialang.org/en/v1/base/math/#Base.mod1

[2] https://docs.julialang.org/en/v1/base/arrays/index.html#Base...

I'm not really convinced by Dijkstras paper. He is basically saying indexing from zero is more natural because if you have an array of natural numbers including zero, then the range of numbers [0..n] is denoted by the index [0..n] which is logical. With 1-indexing you have have to write [1..n+1] to get the values [0..n] which is weird and ugly. Sure, but this assumes that the array in question is starting with 0 in the first place! The whole argument is begging the question.
One reason I really like the 1 based indexing is that I can have a UInt index and 0 can act as a sentinel value. Really nice for writing things like vector embedded linked lists.
> Julia is everything python could have been

The goals of Python were quite different from the goals of Julia.

I’m not sure what Python’s goals are to be honest. It seems to me that the language is outclassed in every way by better, more consistent, more powerful, and more performant languages.

Python programmers seem content implementing the same things over and over again. Like, for example, flattening a list/monad.

List of things python doesn’t have but should: pattern matching, multi-line lambdas, more data structures (look at Scala for an example of what kind of data structures a standard library should provide), real threading, options, monads, futures, better performance, and more.

"In a 1999 report, Van Rossum highlighted the following as his goals for Python:

It should be an easy and intuitive language, just as powerful as major competitors.

It should be open source, so anyone can contribute to its development.

Its code should be understandable as plain English.

It should be suitable for everyday tasks, allowing for short development times."

https://www.computerhistory.org/fellowawards/hall/guido-van-...

“The first sound bite I had for Python was, "Bridge the gap between the shell and C."

So I never intended Python to be the primary language for programmers, although it has become the primary language for many Python users. It was intended to be a second language for people who were already experienced programmers, as some of the early design choices reflect.‘

https://www.artima.com/intv/pyscaleP.html

Interesting list, thanks. FWIW, my view on these..

- I agree about pattern matching, that'd be nice.

- Multi-line lambdas haven't been important, but maybe I'm missing something.

- The list of data structures in the stdlib doesn't matter to me, since the 200k libraries on PyPI make up for it, and since packaging is easy nowadays with Poetry, they are as good as built-in but they get more frequent fixes and improvements than would be possible for the stdlib. Maybe there are some good side-effects of having extra types built in, based on a community of people using these types?

- Threading, I suppose, though Python isn't really the right language overall for that stuff anyway.

- Options, and monads, yeah that'd be nice.

- Futures are an idea whose time has come and gone IMO, but they're in asyncio anyway :\.

- For performance, PyPy is quite fast for many use cases.

Is there a language that has all these things built in and has a repl?

In my opinion, it's an unfortunate accident that Python became popular for numerical / data-ey workloads. It's good for some things, but fast low-overhead loopy code is definitely not one of them!
CPython, yeah :\

I wish the pypy team had finished numpypy. Then fast numerical programs could be written in python instead of relying on all kinds of C extensions and stuff. Python would be great for numerics then.

Does it have a type checker like mypy? Python's protocols (https://www.python.org/dev/peps/pep-0544/) are "structured enough for large programs".
Julia has static typing.

I wouldn’t go that far and say Python is suitable for large programs. It’s clearly not. Working on a large python code base is hell.

> Julia has static typing.

Julia has its own type system, which doesn't conform to the traditional static/dynamic divide. But AFAIK it doesn't have a compile-time type checker like mypy to help me catch type errors early.

Yeah, Julia’s type system is really just not designed for the sort of error catching patterns people seem to want static type systems for. Instead, Julia’s type system is designed for enabling multiple dispatch which I’d argue is a much greater boon than the dubious claims of error catching due to static type systems.

However, you can simulate static typing with the @inferred macro from Test.jl. It will throw an error if all types are not infer able ahead of time which is essentially static typing.

I'm not sure what you mean about 'dubious claims of error catching'. Mypy catches my errors all the time. Sometimes I have to fight with it, but it's definitely worthwhile if I care about having a working program. And when I don't care, I can just ignore any overzealous warnings.
Old Python, before mypy, attrs/dataclasses, etc., is a pain. Nowadays with modern tooling, it's terrific.
How much do you leverage the REPL when you're developing? In my experience with "old Python" (I've yet to update my resume with new Python) I developed in an incremental manner making heavy use of the REPL, I never had the pains I do in static languages without a REPL story. Your comment to me reads like "Nowadays with modern tooling [that lets me develop Python like Java developers develop Java], it's terrific." My day job is Java with a powerful IDE that is crucial for my productivity, but this is because I'm trapped in the "write a comet mass of code interacting with a solar mass of other code and lean on the compiler and IDE tools in addition to my reason to tell me it has a chance of working" way of developing rather than sending small chunks of code to a REPL, testing immediately, and building things up. If I had to (or only knew how to) develop old Python in the Java way I'd probably go mad too. TDD can almost sometimes substitute for a REPL in JavaLand, but it has its own flaws... (And yes modern Java now has something like a REPL, my work environment unfortunately hasn't updated to support it. Soon.)
I don’t think static typing is really related to a repl. There are many statically types languages that provide great repls: Haskell, OCaml, Scala, etc.
I use the repl plenty. I can write the types after I figure out what I'm writing, or before, if I want them to guide me.
Sounds closer to a lisp workflow then, excellent! Thanks for replying as it helps me update away from the notion that it's typically the development style that leads to pains in dynamic languages more than the dynamic/static divide itself and people's preferences.
this. I find it hilarious that people keep comparing a python release from 2010 with something that was released in O(months). Modern python - mypy, attr/dataclasses has come a long way. Numba has also come a long way since it's initial release.