My mental load using Julia is much higher than, e.g., in Python. How to reduce it?

I think these are all reasonable points, but I hope I can help a little bit.

I think what you’re talking about is “fluent interfaces”, and it’s true that we don’t really do that in Julia, at least not in the same way. You might find the |> operator useful, since you can do:

x |> f |> g

as an alternative notation for g(f(x)). This should become even better when we (eventually) get RFC: curry underscore arguments to create anonymous functions by stevengj · Pull Request #24990 · JuliaLang/julia · GitHub which would allow you to write:

line |> mirror(_, along=axis) |> drawInContext(_, ctx)

which is pretty close to the fluent interface but is in some ways even better. Notably, in a fluent interface, there is no way to chain a function that isn’t a method of the returned object. So if sort() is a generic function (and not a method of whatever drawInContext returns), then you can’t do:

line.mirror.drawIncontext(ctx).sort()

but in Julia, you certainly can:

line |> mirror |> x -> drawInContext(x, ctx) |> sort

or, in the future:

line |> mirror |> drawInContext(_, ctx) |> sort

This actually gets at something that I definitely struggled with when coming from Python, and which you’ve mentioned as well, which is that it can be hard to figure out what you’re supposed to do with a given object in Julia. It’s true that in Python I would often type ax.<tab> to get some sense of what I can do with an object. But there’s a downside of that: the things you see when you do ax.<tab> are almost never a good representation of all the things you can do with ax, they’re just the methods it happens to define.

For example, given a list in python, we can sort it with its sort method and reverse it with its reverse method. But what if we want to enumerate it? Well, that’s actually enumerate(x) instead of x.enumerate(), but you’d never find that from x.<tab>. And what if we want a reversed copy of the list, instead of doing it in-place? That’s reversed(x), but not x.reverse(). In Julia things are more consistent: we have enumerate(x) and sort(x) and sort!(x). One isn’t somehow more special than the other by virtue of being a method rather than a generic function.

I do agree that methodswith is kind of an awkward tool to use–this seems like exactly the kind of thing that better editors and interfaces will help with. We’re not there yet, but I think it can be done.

Yeah, I don’t love this. Inside packages I try to have between 0 and 1 plain using Foo statements, with all other imports done explicitly as:

using Foo: bar, baz

which makes it easier to see where things come from. However, it’s worth noting that using is not nearly as bad as from Foo import * would be in Python. from ... import * in Python is bad because:

  1. It makes it hard to tell where things come from
  2. It can silently replace things in your current namespace and break your code in crazy ways. If package Foo has a function called sin and you do: from Foo import *, you’d better hope you weren’t relying on some other definition of sin.

Issue 1 still applies to Julia, but issue 2 does not. Julia won’t let you accidentally blow away things in your current namespace just by using some package:

julia> module Foo
       export sin
       sin(x) = 1
       end
Main.Foo

julia> using .Foo

julia> sin(5)
WARNING: both Foo and Base export "sin"; uses of it in module Main must be qualified
ERROR: UndefVarError: sin not defined

for that reason, I think using in Julia is perfectly acceptable, but I agree that we should limit our use of it in packages.

51 Likes