sodapopcan

sodapopcan

Proposal: Have the formatter put `with`'s `do` on its own line

So after complaining about this for the third or fourth time on this forum, I figured I should make a proposal.

TL;DR

with can be hard to read sometimes as it can be hard to pick out where the clauses end and the body begins. While it’s syntactically valid to put the do on its own line, the formatter forces it back up on the same line as the last clause.

The Problem

While this is a very simple proposal, here are a truckload of words describing it in detail:

A non-insignificant number of people seem to have a problem with with. I think these people fall into two camps. The first has the people who are pipline obsessed who wrap their code or create macros that will make their everything they do pipeable. This proposal is not about this camp as that is a completely separate issue. The second has the people who very much like with as a construct but find the syntax a little clunky at times. This is evident by libraries like happy_with. While I don’t use any of these libraries myself, I am a member of this group.

For me the problem with with isn’t that I have to put commas or that I have to use <- (never understood this complaint) but simply that when there are more than a few clauses and the lines get a little longer—and especially when we aren’t necessarily matching on tuples—it is visually very difficult to find the do, ie, where the clauses end and the body begins. Having played around with it, I’ve found that the very simple act of putting the do on its own line completely solves this (for me).

I’m going to start by sharing this particularly hairy example from LiveBeats. I’m sure this will result in some saying, “Well I would find a different way to write this,” and I think I would try to too (but I haven’t) but this isn’t the point.

Here it is:

with version when version != :invalid <- lookup_version(version_bits),
     layer when layer != :invalid <- lookup_layer(layer_bits),
     sampling_rate when sampling_rate != :invalid <-
       lookup_sampling_rate(version, sampling_rate_index),
     bitrate when bitrate != :invalid <- lookup_bitrate(version, layer, bitrate_index) do
  samples = lookup_samples_per_frame(version, layer
  frame_size = get_frame_size(samples, layer, bitrate, sampling_rate, padding)
  frame_duration = samples / sampling_rate
  <<_skipped::binary-size(frame_size), rest::binary>> = data
  parse_frame(rest, acc + frame_duration, frame_count + 1, offset + frame_size)
else
  # ...
end

So yes, this is a little hairy, but I find that the meat of my negative reaction I get looking at this is that my brain can’t immediately identify where the clauses end and the body begins. The indentation makes it feel like someone just forgot to format which causes quite the visceral reaction in me along with a bit of mild panic! Sounds dramatic but I’m being sincere.

Here is it with do on its own line.

with version when version != :invalid <- lookup_version(version_bits),
      layer when layer != :invalid <- lookup_layer(layer_bits),
      sampling_rate when sampling_rate != :invalid <-
        lookup_sampling_rate(version, sampling_rate_index),
     bitrate when bitrate != :invalid <- lookup_bitrate(version, layer, bitrate_index)
do
  samples = lookup_samples_per_frame(version, layer)
  frame_size = get_frame_size(samples, layer, bitrate, sampling_rate, padding)
  frame_duration = samples / sampling_rate
  <<_skipped::binary-size(frame_size), rest::binary>> = data
  parse_frame(rest, acc + frame_duration, frame_count + 1, offset + frame_size)
else
  # ...
end

I still have that “Whoa, ok what’s going on here,” but I feel way calmer and more prepared/willing to get right to reading through it and figuring it out.

So here’s a much simpler example:

with {:ok, contents} <- File.read("/Users/andrew/passwords.txt"),
     lines = contents |> String.split("\n") |> Enum.map(&String.split/1),
     ["My Bank Account", password] <- Enum.find(lines, fn [label, _] -> label == "My Bank Account" end) do
  do_a_hack(bank_account, password)
end

But even here, while the indentation certainly helps, I still get that “looks like a botched formatting attempt” feeling, which is distracting.

I think is better:

with {:ok, contents} <- File.read("/Users/andrew/passwords.txt"),
     lines = contents |> String.split("\n") |> Enum.map(&String.split/1),
     ["My Bank Account", password] <- Enum.find(lines, fn [label, _] -> label == "My Bank Account" end)
do
  do_a_hack(bank_account, password)
end

Current Solutions

Using parens, the formatter leaves the following alone:

with (
  {:ok, contents} <- File.read!(filename),
  true <- String.contains?("foo")
) do
  do_something_with(contents)
end

This is better but I think the non-paren version reads better. And of course having to add parens to such constructs is not very Elixiry.

Solution

Have the formatter put do on its own line. I’m specifically proposing that the formatter forces this style. I would be somewhat happy with (no pun intended but I’m pretty happy about it) the formatter accepting both styles, but of course that starts to degrade the usefulness of a formatter.

Thanks for reading!

EDIT: Just fixed some code sample errors and a small bit of wording.

Most Liked

christhekeele

christhekeele

I like this proposal.

aragorn-ii-elessar-sword

I wonder if the current implementation of the formatter makes it hard to detect when a single-cause with would be able to fit do on the same single line as with, which is why it defaults to simply clinging to the last clause. Since, obviously, the below is undesirable:

with {:ok, contents} <- File.read!(filename)
do
  do_something_with(contents)
end

A similar formatting outcome happens a lot with long function clauses so I imagine they use a similar heuristic, see:

Many guards
def multi_transaction_lock(multi = %Ecto.Multi{}, scope, id)
    when is_atom(scope) and is_integer(id) do
  multi
end

VS

def multi_transaction_lock(multi = %Ecto.Multi{}, scope, id)
    when is_atom(scope) and is_integer(id)
do
  multi
end

Or even, from the very same LiveBeats function

Much pattern matching
defp parse_frame(
         <<
           0xFF::size(8),
           0b111::size(3),
           version_bits::size(2),
           layer_bits::size(2),
           _protected::size(1),
           bitrate_index::size(4),
           sampling_rate_index::size(2),
           padding::size(1),
           _private::size(1),
           _channel_mode_index::size(2),
           _mode_extension::size(2),
           _copyright::size(1),
           _original::size(1),
           _emphasis::size(2),
           _rest::binary
         >> = data,
         acc,
         frame_count,
         offset
       ) do
  with...
end

VS something more like


defp parse_frame(
  <<
    0xFF::size(8),
    0b111::size(3),
    version_bits::size(2),
    layer_bits::size(2),
    _protected::size(1),
    bitrate_index::size(4),
    sampling_rate_index::size(2),
    padding::size(1),
    _private::size(1),
    _channel_mode_index::size(2),
    _mode_extension::size(2),
    _copyright::size(1),
    _original::size(1),
    _emphasis::size(2),
    _rest::binary
  >> = data,
  acc,
  frame_count,
  offset
) do
  with...
end

More generally, I’d personally prefer the do on its own line in all the above multi-line cases, under the philosophy that indentation is closely aligned with the lexical scoping of blocks.

do often delineates pattern-matching constructs (which have their own scoping rules) on the left from bound code on the right; and in multi-line cases having the do on its own line “resets” my mental expectations of the lexical scope when scanning code, which I think is an accurate mental model of these constructs.


I bet it is possible to develop a formatter extension that applies this rule to just multi-clause with expressions, or even to all multi-line do blocks as per my preference—I’d be interested to give such an extension a go, if anyone develops one to trivially preview how this proposal looks on real-world codebases.

smathy

smathy

My real point here is just that it’s likely that there’ll be people who feel strongly, and arguments and edge cases on both sides that will be difficult to accommodate.

I don’t have a horse in this race, so the following are just general thoughts:

  1. The comma thing is the same issue with the trailing do anyway, so that’s not a comparative negative for your new line do. I rely on my diff tools to make any reordering of attributes easier to see, it’s a shame github doesn’t have a more reliable “color-words” implementation, but I generally review code locally anyway.

  2. My approach to reading code is no doubt heavily influenced by the fact that I started coding in an 80 column monochrome world. In fact more often than not the first step to reading any sizable source code was to send a print job off to the dot matrix.

  3. I have been told I read code like a compiler.

  4. Wide screens and editor tooling means that I really can’t relate to the feelings you’re expressing about readability difficulty. Ie. the two big messy original examples you showed look the same to me. I’m not unfamiliar with your position, I’ve heard it from many others, it’s just not something that impacts me. The only thing that threw me reading that first example was the missing ) in the lookup_samples_per_frame call.

  5. I’ve often wondered whether it might be a good learning exercise for developers to switch off all the color and helpers we’re so used to (reliant upon) these days, and just get some experience reading black and white source code.

  6. I secretly wish I could convert the world to my way of thinking about this, because all discussions of style feel like bikeshedding to me. That “panic” you talked about hits me when I get invited to a meeting to discuss style :slight_smile:

  7. There are definitely some things which I believe are inherently hard for anyone to read, such as your assign/2 reordered attributes thing. Obviously big diffs where only indenting has changed fall into the same bucket.

tomekowal

tomekowal

I have another problem with the formatting of with. It is somewhat artificial and solved by tooling, but it is annoying from time to time. Functions between with and do are the only thing that is indent odd amount of spaces :stuck_out_tongue:

This is annoying when I work with editors that have commands like indent that atumoatically put two spaces. Unlike if and case that work with one expression before do part, with is designed to work with many. So I’d actually see it like this:

with
  expr1 <- func1()
  expr2 <- func2()
do
  expr3
  expr4
else
  :a -> a
  :b -> b
end
smathy

smathy

Heh, I often say to devs who seem reluctant to “write tests” that every developer writes tests, the only question is whether you save them so you can rerun them.

I think it’s awesome how Elixir has that copy/paste from iex to test feature, whatever it’s called.

Ah, so you were. Yeah, I’m not sure I’ve found it exhausting, but I probably would if I was forced to persist with it, but my #1 complaint would be that I’m unproductive.

That process seems more achievable than going straight to TDD from the get-go. You’ve kind of done the “tinkering/playing around” phase I was talking about in my own process before getting into the TDD.

smathy

smathy

Yeah, my justification is that if they’re using terms that they have no idea about, then my answer really doesn’t matter:

Where Next?

Popular in Proposals: Ideas Top

MeerKatDev
many times we do stuff like (e.g. test setups, json views in phoenix) aaa = ... bbb = ... ccc = ... %{aaa: aaa, bbb: bbb, ccc: ccc} and...
New
rhcarvalho
Hi all, I would like to gather some feedback before a more intentional proposal to add a new :depth option when specifying a Git depende...
New
rekkice
I’m building an editor integration to evaluate Elixir code in an IEx session. While Code.eval_string/3 allows tracking variable bindings,...
New
kip
Sumary of proposal DateTime.from_iso8601/3 adjusted to: set all numeric fields to the values as parsed (not shifted to UTC), preservi...
New
pdgonzalez872
Hi! There has been some discussion about hiring/jobs on here and I thought about running this by everyone. I wanted to try to help recr...
New
dli
Ecto currently supports some data-modifying WITH statements / CTEs for Postgres: Options: […] :operation - one of :all , :update_all ,...
New
winsalva
Hello all. First of all i’m running this using termux on an android phone. Running mix assets.setup shows this message 06:54:08.450 [de...
New
bartblast
This could resolve to {[a: 1, b: 2]}. Was it ever considered to allow such syntax? Notice this: {:abc, a: 1, b: 2} and this: my_fun(:abc,...
New
sezaru
When writing my code, I always find __MODULE__ very useful to use as alias of that module “inner dependencies”, ex: alias __MODULE__.{Im...
New
dkuku
This is a proposal to make the map key mismatch errors a bit better: Every time I have a typo It’s very challenging for me even when I u...
New

Other popular topics Top

SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement