Yes, your understanding is correct. This proposal is only about methods — it doesn't affects functions.
Perhaps more correct would be to say that queries don't modifies self
on its original location.
That's why methods with signatures like (self, ..) -> Self
are treated as commands: in current Rust most often they're used as x = x.self_consuming_method()
so in fact they modifies self
on its original location but that's done on the call side, hence, to keep this proposal consistent the same expression must be x self_consuming_method
.
If talking about the fact that (&self, ..) -> T
and (self, ..) -> T
methods uses the same notation IMO there's just nothing useful in separating them. That said, we may get the local information that self
was/wasn't moved into this method but why do we need it? Either in let len = vec.len()
and in let slice = vec.leak()
the most interesting thing is return value, so users should focus on it and not on ownership/borrowing in RHS expression. Something like vec->len()
would neither reduce the number of mut
in code nor it would make returning &mut Self
from methods obsolete thus it's really unmotivated syntax.
It's only about modifying the receiver — the rest of arguments don't interact in any way with the proposed syntax. This is how method cascading works, but also it could be said that the rest of arguments already in most of cases are mutated explicitly e.g. something.fmt(&mut formatter)
so any interaction is unnecessary.
Right, "query" is the default syntax: it's either familiar and encourages immutable programming style, so it's also the preferred syntax. However, I'm not sure about restricting #[non_query]
to queries only: it seems that more useful attribute would be #[interior_mut]
since it won't be wrong to apply it to requests and commands in order to make updates to atomics, ref-cells, etc. explicit and either to get rid of another corner case. Anyway, tweaking this behavior seems to be out of scope of this proposal, so your intuition is correct.
In theory misused method syntax is a subject for compiler warnings, and there either should be an attribute like #[allow(uniform_method_call)
or something like that to make these warnings suppressable.
- I see nothing wrong with making a query called through request notation to compile — user may just forgot to add
#[interior_mut]
or plans to add it later so we shouldn't punish him with recompiling the code - I see nothing wrong with making a query called through command notation to compile — again user may consider to change the API and there might be IDE intention to make this change more ergonomic
- Calling requests with command notation is allowed unless it's annotated with
#[must_use]
— that's obvious, and you've already agreed with that - Allowing requests called with query notation to compile is important for backward compatibility — methods with signatures like
(&mut self, ..) -> &mut Self
when their return value is used (basically any builder pattern usage) would be treated as requests, and if someone migrates to edition with cascades e.g. rewriting macros to make such method calls to compile might be a lot of additional work
- Allowing commands called with query notation to compile is important for the same reason — if every
(self, ..) -> Self
method chain in macros will stop to compile it might be a big disruption; P.S. perhaps we should allow method chaining on(&mut self, ..) -> ()
methods on older editions to make them compatible with APIs written in later editions — after migration they should compile either. - Compiling commands with request notation is also important for consistency — this means that we use the
()
return value for examplefn x(mut v: Vec<i32>) { v~push(0) }
could be either represented asfn x(mut v: Vec<i32>) { v push (0); }
although the later is a proper style
Like it or not but even in current Rust changing method signatures is most likely a breaking change for users. And IMO alterations between request/command/query are important enough to force users to review every usage like we currently do with adding another enum variant for example.
On the other hand side the proposed syntax reduces a number of unpleasant breaking changes like transferring consuming builder flavor to non-consuming and vice-versa (feels bad when you've used the language where such problem doesn't exist), or changing chainable API to non-chainable and vice-versa.
This is correct, although I'm not sure what you mean with " hardcoding some specific "usefulness" check" — if the value is used then it's automatically useful?
This is also correct
Unfortunately, you still miss a lot of key parts where the most significant is that cascade syntax doesn't have a single special feature but many of them — that's why you fell in the same "not worth the churn" trap as everyone else in this thread.
At first, since command call is guaranteed to result in its receiver it implies that it's also guaranteed to result in the same type. For example we aren't sure what type Foo::new().bar()
returns, in contrast Foo::new() bar
makes locally visible that it returns Foo
. And this property holds for arbitrarily long chain of cascaded method calls e.g. in builder usage or any other DSL this will be visible, then .build()
or ~build()?
will indicate that a different type will be returned at the end (and not somewhere in the middle). Moreover, the suggested notation without extra operators and without ()
amplifies this effect — with it users would focus on continuity of cascade chain and not other things that may suggest change of underlying type or anything else (perhaps this is how I could motivate the adjacent command notation?).
It's hard to argue against that knowing what type method returns is useful when learning APIs, so yet cascading isn't only about chaining non-chainable methods and revealing mutability.
Furthermore, I've made "the same type guarantee" available literally everywhere that said if method baz.qux()
currently returns the same type as its receiver then with proposed syntax we 100% will see that:
- if it mutates then it will require the command syntax so it will look like
baz qux
and users would see that no new type was introduced into the scope — we already know that - if it doesn't mutate that's a bit more complicated: to preserve type identity using
baz qux
notation would requirebaz
to be mutable so it's no go, then it's possible to imagine something like{baz} qux
but it wouldn't parse because it's ambiguous with "expression followed by identifier" (not a big problem BTW), hence, the minimum amount of symbols to keep type information might be({baz}) qux
which isn't ergonomic- So I've introduced
(baz) qux
as shorthand for({baz}) qux
which is either supposed to be enforced by compiler instead ofbaz.qux()
- That shorthand doesn't work with
(&self, ..) -> &Self
methods although I've never seen such methods anywhere in the current Rust, so we don't care about them
- So I've introduced
And for everyone's surprise this is very consistent syntax e.g. on (x) saturating_add (z)
both (x)
and (z)
behaves identically and unlike with x.saturating_add(z)
there's a beautiful symmetry that makes it very close to x + y
. So, we either have type information and a better notation!
This allows to guess how this introductory example works and why it should look exactly like that:
let x = (x) sin;
let y = (y / 2) cos;
let z = (x + y - z) wrapping_add (4) tan;
// In current Rust:
let x = x.sin();
let y = (y / 2).cos();
let z = (x + y - z).wrapping_add(4).tan();
It's still not convincing example because this is imaginary code — sorry about that; for a long time I've been unable to find a decent chunk of code which extensively uses mathematical operators, so this is what I've came up with. Only recently I've discovered this gist which allows to create a very representative example which IMO beats every "readability argument" over there (click on image to enlarge):
I will repeat that command notation either is guaranteed to reveal mutations made with methods plus its guaranteed to reveal type identity of subjects of command chains — both are very useful when learning unfamiliar APIs; keep in mind that for newcomer every API is unfamiliar! This isn't only the ability to chain non-chainable methods like Dart's ..
operator was.
At second, command notation is made in a way that removes the distinction between consuming builders, non-consuming builders and temporary mutability idiom — these three concepts are replaced with a single. IMO currently they're just workarounds over rough edges of the language, and they're confusing for a person who learns Rust because they exists without any useful purpose. APIs either may migrate from one style to another and this creates unnecessary friction even for people who already knows Rust in perfection.
This is how cascading resolves the situation:
- In current Rust we need
(&mut self, ..) -> &mut Self
methods on builders to make setters chainable but with the proposed syntax it would be enough to have(&mut self, ..) -> ()
— already some simplification. - Then because with current syntax builder setters usually returns
&mut Self
it's impossible to appendfn build(self) -> Thing
at the end of such chains onlyfn build(&self) -> Thing
that clones fields of builder is possible; that's why currently setters with(self, ..) -> Self
signatures sometimes are necessary to transfer ownership to.build()
call. And because cascades are guaranteed to return in their input type we can obtain ownership ofself
at the end so only(&mut self, ..) -> ()
setters will have sense to use — another simplification. - Next implementing methods with
(self, ..) -> Self
signatures would have sense only forCopy
types taking which by reference isn't wise e.g. numbers. With that such methods implies that type implementsCopy
and we may even turn it into guarantee — yet another simplification. - Finally APIs like
Vec::push
becomes chainable by default so workaround like implementing chainable wrappers or using temporary mutability are unnecessary. We also prevent people from intentionally implementing non-chainable APIs because "method chaining is confusing", since with distinct request/command/query notations such confusions cannot occur — the last simplification.
As we see, this notation adds a lot of defensive design into the language, so problems that people coming from garbage collected languages aren't prepared to deal with just cannot exist. IMO, it's better to spend time on learning some language features than on rewriting your code again and again.
At third, since it removes temporary mutability idiom it also removes mut
annotations during data initialization, so code just becomes more ergonomic to read and write:
let vec = Vec::new()
push (1)
push (2)
push (etc);
// Vs current
let vec = {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(etc);
vec
};
// Or with a bad style
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(etc);
Recently I've realized which another mega interesting possibility it opens: this might finally allow IDEs to insert mut
automatically when it's needed: currently it's required to be specified manually because programmers must first express the intention to mutate something otherwise it may lead to accidental mutations that we don't want, so another negative effect of implicit mutations under method calls... Through selecting a mutating notation (command or request) programmer communicates this intention clearly and even if this notation was inserted by IDE it's still nearby to notice, so after that adding mut
becomes a burden which IDE should take care of — quite a big ergonomy and productivity gain!
At fourth, there's the formatting
Foo::new()
bar (
baz,
Baz::qux()
quux (
quux,
Quux::new(),
, )
, )
which:
- Gives a very little possibility to diverge: it can be extended only in vertical or horizontal direction
- Could grow to gigantic scales (e.g. view in GUI DSL) while remaining easy to navigate through
- Requires twice less indentation than the current syntax
- Makes the descending ladder of closing braces/brackets/parentheses twice as less steep
And I think the , )
and ) )
and } )
at the end of expression are important like Ok(())
because it shows the context where you was — no need to scroll up and read method name in order to recall it. Moreover, it's a very good target for IDE hints: its easy to hover on so that may reveal the type which is returned by this cascade, and on click it might scroll to the beginning of cascade — this this should be useful in GUI programming where methods could be chained and nested very extensively and currently its easy to get lost.
However, even in simpler code this formatting has a positive effect: it by design prevents placing too many things on the same line which IMO is quite a problem in the current Rust — such antipatterns are just too subtle and could easily skim through code reviews. I have a very good example of that built on smithay's code:
Hence, there's something very beautiful about geometry which the proposed construct takes.
At fifth, we could see that this formatting plays very nicely with closures, so similarly it plays very nice with any other scope e.g. from control flow constructs; as we remember from .await
proposal period there was some desire in the community to have some sort of general pipelining e.g. .match
, .for
, .if
etc. — the proposed syntax makes this dream closer to reality. Again, a possibility like that would be mostly useful in GUI programming to avoid temporary bindings in layout trees that could disrupt declarative flow of such expressions and just looks confusing because their declaration would be placed too away from the usage.
The prior art for that feature is Dart's control flow collections feature as well as mentioned above Kotlin's scope functions, but these were prone to abuse and somewhat complex.
I have much simpler vision on that feature:
TabBar::new()
is_scrollable (false)
also (
for tab in tabs {
super tab (Tab::new() text (tab))
} )
Here super
gives mutable access to the receiver of nearest method call (not function!), so we've achieved a general pipelining that works perfectly with ownership/borrowing, looks bulky enough to be abused, doesn't require immediate concepts like functions/closures to be inserted to make e.g. branching/looping to work, and what's the most important doesn't require users to learn about currying/partial application and other complicating stuff from functional programming world — as long as you know about cascades, receivers and basic Rust it's obvious how it works.
Desugaring for the above expression would be the following:
{
let mut x = TabBar::new();
x.is_scrollable(false);
x.also({
for tab in tabs {
x.tab({ let y = Tab::new(); y.text(tab); y })
});
x
}
While we might imagine that the signature of also
method is this:
fn also<T>(&mut self, t: T) { }
Although, I plan to implement it differently because here &mut self
will lock the receiver as unique so x also (dbg!(super))
would be required to written as x also ({ dbg!(super); })
which isn't ergonomic. A better opportunity exists to achieve the expected result: we make *const self
receivers to compile (this doesn't work currently for some reason, perhaps it's very unsafe to dereference such receivers thus I don't propose that to allow — only methods should be able to take raw pointers to enable cascading/chaining)
fn also<T>(*const self, t: T) -> T { t }
And then this signature allows to unify all methods of tap crate except specialized ones like tap_ok
, tap_err
which anyway could be replaced with something like inspect
, inspect_err
on Option
/Result
itself. More specifically, we would have a single method with very nice name which could be altered between cascaded/call notations to select tap/pipe behavior.
We already have an example how it looks in tapping context, so here's an example how it looks in piping context which I've built on code from druid GUI framework:
So, there are at least five extra special features. Neither of them is more important than another. The main point of this syntax in how well they aligns together and how well they fits into the language and into IDE. It would be easy to discard every of them taken outside of the whole picture. That's why I insist on not focusing on a single thing.