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.























