Runic.jl: A code formatter with rules set in stone

I have been tinkering with a code formatter that I want to announce: Runic.jl.


Sample output from running Runic with check and diff mode enabled on a small code base.

Just like e.g. gofmt (Go formatter) and black (Python formatter), Runic has no configuration. This is a feature that, in particular in the Go community, is very popular which is why I wanted to try this out also for Julia.

Refer to the README for:

I have tested the formatter on all julia code in base and all julia code in my .julia folder without errors (although I have not visually inspected all those tens of thousands lines of code, of course) so I would consider it safe to start trying out.

Note that the package is not yet registered so ideas for more transformations or feedback on current transformations are very much appreciated! Please open an issue if you have any type of feedback.

Thanks!

67 Likes

Thanks for contributing this great package!

Also, I very much agree, that the absent configurability can be a huge advantage.
Of course formatting style is a matter of taste and the applied style does not need to (and certainly will not) be everyones favourite.

Also, I find all style choices at least reasonable.

Or rather all except one…

I feel like spaces around keyword arguments really decrese legibility.
Also, I have never really seen this style in the wild and thus would be interested in why you made this decision?
Do you prefer it like that? Do others prefer it like that? Or is it for the sake of parser simplicity and/or pragmatism, handling all =s in the same way?

If this is due to the latter, would you consider changing this - if an overwhelming majority of users have similar reservations?

However, irrespective of my critisism, I could see myself employing Runic.jl as default formatter. :smiley:

EDIT: After some healthy rumination, it became clear to me that my primary objection to space-flanked kwarg-= is in function definitions/calls formatted as a single line.
As soon as the formatting is one line per kwarg I might still not find it appealing, but in terms of code legibility I think it’s perfectly fine.

6 Likes

Thank you for having a look and for the comments.

This is actually the original reason. This comes as a result of me implementing the “spaces around assignments” rule without restricting the context of it and realize afterwards it also happened to apply to keyword arguments in function definitions and function calls.

It looks like at least SciMLStyle recommends it (although it is a bit cryptic).

I realized after trying Runic on some code bases that I was very inconsistent. For example I think I (used to) prefer no spaces around “simple” keyword arguments like e.g. foo(; bar=true) but not more complicated ones like e.g. foo(; bar = baz(foo + 3)) etc. I think that now I have come around to always use spaces because of consistency and because it seems kind of arbitrary to not use spaces in this specific context but everywhere else.

8 Likes

Since you’ve mentioned “context”, I wanted to ask: Are there contextual transformations that would be in scope for this project?

I’ve never been a fan of Julia being team TMTOWTDI, and I wonder how practical it’d be to have a tool that does more complex normalizations. For example, transforming

-(foo; bar; baz)
+begin
+    foo
+    bar
+    baz
+end

but also:

-:(foo; bar; baz)
+quote
+    foo
+    bar
+    baz
+end

and things like

-() -> begin
+function()
    foo
    bar
    baz
 end
2 Likes

I also usually prefer no spaces, but one argument for spaces is that they prevent syntax ambiguity errors. Specifically, if the keyword ends in a bang, like foo! = bar, the no-space version is parsed as foo != bar, which is obviously not valid keyword argument syntax.

5 Likes

Well, obviously, a package like this invites lots of comments because everyone’s preferences are different. But I can’t help myself :smiley:

- function test(a::Union{Int,Float64}, b::T; foo=:bar) where {T<:Number}
-     # body
- end
+ function test(a::Union{Int, Float64}, b::T; foo = :bar) where {T <: Number}
+   # body
+ end

Personally, in addition to kw args not having spaces I really like not having spaces within { } blocks, which is also part of some existing style guides.

For me, there are two reasons for that:

  • Function signatures can be relatively complex without going crazy in length.
  • Spaces are about separating different “semantic” blocks in an expression. I think function signatures should highlight the separation of arguments first. Type information and keywords are tightly bound to the respective argument

I think { } blocks, in general, tend to be messy, especially when nested. Let’s embrace the chaos and omit spaces, they’re ugly either way!
But of course everybody taste is different, which is why I quite like the approach of not having any options and would consider using your package, regardless of the rules you settle on.

Big up for the readme, it’s super nice that you included tutorials for git hooks and GitHub actions!

5 Likes

Obviously it’s all very subjective but I think the fact that these signatures can be complex and {} blocks can be too is, in my opinion, even more reason to want spaces.

And Runic is really nice, thanks for this great contribution @fredrikekre! I’m really pleased with how it looks in my code.

3 Likes

Agreed on this point. I’ve never understood why the reasoning that spaces around = improves readability should not apply to keyword arguments.

Not that I will use Runic anyway, since the multiline formatting is a dealbreaker for me.

3 Likes

Thank you for this package, this is very close to my preferred style so I am considering using it going forward. Small issues I noticed trying it out:

In some situations involving macros, Runic seems to change meaning. For example, in

using Catalyst
net = @network_component net begin
    r, X --> Y
end
net′ = extend(net, @network_component (@species Z(t);))

Runic removes the ;, making the syntax invalid.

Another issue is related to linebreaks after =, both for short-form function definitions and variable assignments. Runic changes

y =
    if x
      1
    else
      2
    end

to

y =
    if x
    1
else
    2
end

which is probably not intended.

(The only style choice I disagree with is that

function f(
    x
)
    x
end

is formatted as

function f(
        x
    )
    x
end

because I feel that the ) should be indented just like the start of the line containing the matching ( and the double indentation is unnecessary in my opinion. Ideally I would also prefer

f([
    a
    b
])

to be allowed, that is, Runic not forcing a linebreak before [ in situations like this, but I could get used to it.)

2 Likes

This is also such an arbitrary rule though. Why spaces in lists surrounded by () and [] but not {}?

Can you open an issue about this?

And this too?

The reason I dislike your preferred style there is that arguments and the function body have the same indent which can make it a bit difficult to separate them at times. And the closing ) as the first characther look like it closes the function like a closing } in e.g. C. I like that everything between function and end are indented for this reason.

While this was already my preferred style it isn’t actually directly encoded in Runic but just follows naturally from the two indenting rules saying that i) everything between function and end have an increased indent level of 1, and ii) argument list to multiline function calls (f(\nx\n)) result in another increase of indent level.

In general Runic style isn’t “my” style so I have also had to accept certain formatting results as the come. Instead Runic implement a set of rules and then it applies them ruthlessly everywhere.

5 Likes

Your package, your rules. :wink:
(It’s not a big issue for me, I could probably get used to it.)

2 Likes

I don’t know much about Go, but I am not sure that this is an ideal fit for a language like Julia, which has a very rich syntax. There may not be a single ideal formatting, even for one person, as it depends on the context (how much one wants to save vertical space, etc).

Given all the effort you have invested into writing this package, I wonder how much extra it would take to parse various options from a TOML file, put them in a struct, and use that for emitting output. Cf #12, #34 (semicolons could be useful in code meant for interactive use), and various tiny issues you may encounter in the future for rare corner cases. Instead of having to think about a “right” choice, you can leave it open, and just have a default for the choice you consider most sensible.

1 Like

The point is that there is no ideal formatting, but that global consistency improves readability in new code bases! Any particular decision has tradeoffs in different contexts, but those tradeoffs are far out weighed just making a call and sticking with it.

7 Likes

I roughly agree that consistency in style is good, but IMO style is an aesthetic issue that would ideally be left to the programmer to decide upon on a case-by-case basis, just as programming itself is done.

3 Likes

To a small extent, maybe. That said, I doubt that the very minor choices have a huge impact on readability. Eg I can read both

a = 1
ab = 2

and

a  = 1
ab = 2

just fine. Personally I use the first, but if I am contributing to a codebase that uses the second, I try to do it like that.

It is my impression that the Julia community is much more laissez-faire than Go with its single best way for everything. The Julian approach, IMO, is to go for Swiss army knives that include a chainsaw.

8 Likes

I’m also in the “No way, I’m going to format my code the way I want to” camp. But carry on. :slight_smile:

5 Likes

I imagine the use case is less for formatting your own code and more for formatting pull requests and collaborative code bases into consistency: an automatically enforced style guide.

Then you can write your code however you are used to and let the formatter adjust it later.

1 Like

No doubt. But I still don’t like the idea of my formatting being changed. Luckily for me most of my coding is on individual projects, so I can follow my own style anyways.

1 Like

The README only describes how to make Runic work with NeoVim. Does Runic also work with VSCode?

2 Likes

Yes, but this does not imply that all such projects have the same formatting style down to the tiniest details. Configuration is still needed.

Sorry, I still do not understand why lack of configuration is a feature for a task where reasonable people can have different preferences.

3 Likes