Extended dot operator provides a more flexible way of invoking associated methods, functions, macros, and even control flow constructs, which in some cases allows to avoid extra bindings, parentesis, identifiers, and unnecessary mut
state.
This syntax would be simple, uniform, and powerful substitution for many syntax constructs available in other programming languages, that have more or less the same purpose and similar structure, but presumable in their primary forms would never be available in Rust due to insuitable complexity/usefulness/verbosity ratio.
This syntax allows to move any prefix operator directly to method inside of a method call chain that returns value on which that operator will be applied. In some cases it allows to simplify scoping and improve readability.
This syntax looks completely different than familiar for everyone |>
operator but instead it plays well with move/borrow semantics, has cleaner scoping/precedence, and overall is way more flexible.
This syntax allows to drop method return value and substitute it with previous value in method call chain. In this way it's possible to fluently interact with APIs that don't support method chaining for various reasons.
This syntax allows to initialize values without using one-off mut
bindings. Also, it allows to save on providing initialization macro or implementing builder pattern that otherwise would be used very rarely.
This syntax allows to handle errors in separate chain and some DSLs could adopt it instead of macros for better integration with IDE autocompletion with guarantee that there's no hidded magic inside.
This syntax is the same as for pipeline operator. It's explicitness don't require any modification on macro declaration side, therefore on postfix position we are able to apply all existed macros.
This syntax allows to reorganize tuple "on the fly" without introducing many temporary bindings in destructuring. It's also helpful to simplify tuples that have complex pattern before they would be destructured.
Extended dot construct adds abillity to compose chainable and non-chainable expressions with .
operator. It could be seen as combinator that drags its argument through a series of actions. The difference from regular combinator is that it don't uses closures, has distinguishable syntax, and properly handles all early-returning syntax constructs.
The grammar for it could be expressed as:
Its based on brackets that are put right after .
operator and takes a comma separated list of special expressions.
.
operator is called receiver1. All actions always uses receiver: it's always implicitly available inside of all actions under this
alias that has the same properties as any other regular mutable binding.
2. Action can use receiver implicitly: when action begins with a number, simple identifier, or function call that don't takes this
as parameter, then this.
would be prepended to action producing tuple indexing, property access, or method call respectively.
This implies:
this
this.
3. Explicit receiver becomes unavailable in braces nested inside of extended dot scope: even braces of control flow constructs counts, although in nested brackets, parenteses, etc. everything works as expected .
This implies:
this
occurs inside of nested braces4. Presence of trailing comma determines return value: without trailing comma the result of last action is returned, but with trailing comma it's dropped and receiver is returned instead.
Capturing receiver value:
*here and below _id
only represents a differently named unique identifier.
a.b.[
];
⇒
{
let mut _id = a.b
};
Inserting provided actions:
*warning should be produced when single action is provided and there's no trailing comma after it.
a.b.[
c(),
d,
e(this)
];
⇒
{
let mut _id = a.b
;{ c() }
;{ d }
;{ e(this) }
};
Prepending implicit this
to actions:
*prepending should be skipped for language constructs.
*prepending should be skipped for methods that already takes this
as parameter.
*prepending should be skipped for actions that already begins with this
.
*warning should be produced when action already begins with this
.
a.b.[
0,
c(),
d,
this.0,
this.c(),
this.d,
e(this),
!f()
];
⇒
{
let mut _id = a.b
;{ _id.0 }
;{ _id.c() }
;{ _id.d }
;{ this.0 }
;{ this.c() }
;{ this.d }
;{ e(this) }
;{ !f() }
};
Replacing this
with _id
inside of actions:
*replacing should be skipped for this
placed inside of braces.
a.b.[
this.0,
this.c(),
this.d,
e(this),
!this,
{this},
unsafe { this }
];
⇒
{
let mut _id = a.b
;{ _id.0 }
;{ _id.c() }
;{ _id.d }
;{ e(_id) }
;{ !_id }
;{ {this} }
;{ unsafe { this } }
};
Ensuring that _id
is used inside of all actions:
*when action don't uses _id
then error should be produced
a.b.[
,
(),
{x()},
];
⇒
{
let mut _id = a.b
;{ ERROR }
;{ () ERROR }
;{ x() ERROR }
};
Overriding return value:
*when trailing comma is provided then reciver should be returned instead of last action result
a.b.[
c(),
];
⇒
{
let mut _id = a.b
;{ _id.c() }
_id
};
This RFC provides probably the most compact and the easiest to work with syntax that's possible for the same functionality. Also it resolves many questions about introducing postfix macros, introducing unified method call syntax, and providing combinators that transforms self
.
Without it some features could be implemented in different and in less uniform way, and some features most likely wouldn't be implemented at all.
Alternatives:
this
receiverIn current form this syntax was never used in any programming language before. But the same functionality provides Kotlin through extension methods like run
, with
, let
, apply
, also
, that are available on all Kotlin objects. Overall, experience with them wasn't very good because of generic names that don't describes well intention, difficulty in differentiating them from domain-specific functions, and difficulty to spot from where implicit or explicit receiver comes from. Although, in Kotlin they are very easy to understand after the rest of its idioms, also they looks consistent, and disciplined programmers still would write a proper code.
This RFC takes into account all encountered problems and proposed syntax don't affected with them with price of sacrificing in functionality, a bit more complexity, and differentiating in syntax.
Looks like it's impossible to invent something more here