Second version
Completely different than previous, features:
- Revealing of hidden mutations inside of method call chains
- Flexible syntax for method cascading
- Redesigned pipe-forwarding
- EDSLs
1. Explicit mutations on method call chains
Purpose of that is to make regular functions call consistent with features that will be introduced in next sections, and to prevent temporary mutable bindings to stay accessible in whole scope.
It forces functions taking &mut self
to prepend ~
when invoked.
Don’t confuse that with previous ~
- it’s different and doesn’t change flow but acts more like annotation on method call.
Syntax is:
-
mutable.~function()
- to indicate possible mutation on mutable
value
where mutable
expresses mut binding or function call chain. That’s to not write it twice and the same convention is applied further in proposal.
Using ~
on functionts that mutates frees us from introducing temporary mut bindings inside of method call chain, because it already shows all relevant information:
- Which functions does mutations and on what value
- Where anonymous mutable bindings are introduced
I think that ~
is good sigil here, since it’s short, easy to remember and type, also is consistent with features that will be introduced in next sections, and looks like creased dash which brings some associations with mutation.
Yes, it’s another operator and programmers must learn it. However, reading TRPL is mandatory for every rustacean, so once operator will be defined and documented - nobody will be confused.
Code will look like this:
let mut collection = get_collection(); // `mut` is still required here.
collection.~sort(); // `~` shows where we do mutation.
return Type::new() // Anonymous mutable binding introduced here.
.~mutate1() // This function mutates return value of `new`.
.~mutate2() // And this mutates return value of `mutate1`.
.transform(); // But this don't mutates anything.
Also, that’s breaking change.
Is it worth it - for discussion (but read whole proposal before disclaiming).
2. Side-effects on method call chain
That’s supposed to be alternative to method cascading.
It allows to apply batch of functions that takes self
, &self
or &mut self
to value without breaking chain.
Syntax is:
-
value.>(function(,),)
- to call one or multiple function
s on value
and ignore result of them
-
mutable.~>(function(,),)
- if one or more of function
s needs value to be bound as mut
where trailing comma means that other functions/arguments might be applied.
Let analyze all parts:
-
~
has the same meaning as in previous section - function mutates. Also nicely combines with >
into ~>
arrow.
-
~>
and >
neatly shows direction where value
is passed.
-
.
is required to show that method call and (possible) (de)referencing occurs. It also fills space for proper alignment and keeps chain syntax consistent (I loathe how e.g. |>
breaks chain visually).
-
(
and )
shows that we have limited scope where we can operate on value.
-
(
and )
are chosen over {
and }
to not confuse that syntax with expressions and to not add new symbols in call chain syntax.
-
,
separated functions - it’s to group method calls to not search through chain of .~>
/.>
It implies that function
takes self
by reference or Clone
is implemented for it, since you can’t continue chain or apply other side-effects if subject value was moved somewhere.
How it looks on practice:
let hmap = HashMap::new().~> ( // Anonymous mut bining introduced by `.~>`.
insert("key1", val1), // `insert` is called on `mut HashMap`.
insert("key2", val2), // Also is called on `mut HashMap`.
); // HashMap is returned from parentesis.
let mut collection = get_collection(); // `mut` is required on binding.
collection.~>(sort()); // `.~>` takes `mut collection` into `sort`.
.iter()… // We can continue chain on `mut collection`.
return OpenOptions::new()
.~>(write(true), read(true)) // Single-line representation.
.open("location")?
.to_some_collection()
.~>(sort()); // Single side-effect method called.
let value = get_value()
.> (action_1(), // Value can't be taken as `&mut self` here.
action_2()) // Proper aligning.
Don’t confuse it with various withers
, since it don’t allows to run arbitrary expressions with implicit context.
Only methods chains - nothing more.
3. Calling external functions on value
That’s supposed to be alternative to pipe forwarding.
Here I will start with reasons that shows great need behind it:
- Functional programmers uses it extensively, modern languages have it in arsenal, it helps, and not providing it in Rust is rather restrictive.
- Dense usage of meaningless bindings,
let
, mut
, ;
in code, necessity of imperative style - here is reason why Rust is considered as harsh and verbose language, not abundance of operators.
-
Side-effects on method call chain will be allowed to apply external functions on subject value. This is very important to keep code modular and to make EDSLs introduced in next section extensible.
- Declarative programming style is less verbose and more descriptive, thus safer - that’s why Rust should adopt and promote it.
Syntax is:
-
value.function(,in,)
- to pass value
into function
. Use &in
to pass by reference
-
mutable.~function(,&mut in,)
- to pass mutable
into function
by mut reference
-
value.> (,side_effect(,in,),)
- to pass value
into side_effect
. Use &in
to pass by reference
-
mutable.~>(,side_effect(,&mut in,),)
- to pass mutable
into side_effect
by mut reference
where trailing commas means that other arguments/functions might be added.
It don’t differ too much from regular function call.
But that’s not very important to know, which kind of function we call: associated or external, so visibility is sufficient (and it’s actually good when function has one argument).
in
is choosed because it’s short, descriptive, already used as keyword, and has syntax highlighting.
There can be other placeholder, e.g. it
, this
, that
- decision might be changed.
Examples:
return collection.iter()
.apply_mapping_combinators(in) // Iterator moved to `apply_mapping_…`
.apply_logging_combinators(in) // Iterator moved to `apply_logging_…`
.collect()
return Type::builder()
.~apply_common_settings(&mut in) // To mutate builder.
.build()
long_name_binding
.borrowed(&in, &in, &in) // This is allowed for all types.
long_name_binding
.copied(in, in, in) // This is allowed only if `Copy` is implemented.
let text = String::new()
.~>(put_default_text(&mut in),
ecranize_shell(&mut in))
.> (debug(&in),
send(&in));
Experimental
Support for macros:
get_value()
.start_chain()
.>(println!("start: {}", &in)) // Macros treaten as regular functions.
.continue_chain()
.println!("continue: {}", &in);
Support for constructors:
get_value()
.take_enum(Some(in))
.take_struct(Struct { x: in, y: { in_is_not_visible_here } });
4. Splitted chain
This section is most interesting in whole proposal and has best examples.
It’s idea is simply to allow to call functions further on side-effects results.
Usecases are:
- Error handling from external functions: mapping, unwrapping, using
?
- EDSLs or Kotlin-like type-safe builders: without returning
self
, lambdas, scoping issues, prepending ~
on each &mut self
-taking function, implicitness, and additional boilerplate around
Examples:
return PathBuf::from("base")
.>(fs::create_dir_all(&in).unwrap()) // Here we actually splitted chain.
.~push("filename") // Don't forget - it mutates.
.File::open(in);
let content = String::new()
.~>(file.read_to_string(&mut in)?); // `?` is applied to read_to_string
return Tree::root(0).~> ( // `top-level` started.
branch(1), // Added `branch1` to `top-level`.
branch(1).~> ( // Other `branch1` added to `top-level`.
branch(2), // Added `branch2` to `branch1`.
branch(2), // ...
branch(2).~> (
branch(3),
branch(3),
),
branch(2),
add_other_branches(in), // External function is called on branch
)
);
let matches = App::new("My Super Program").~> ( // Prototype from clap's README
version("1.0"),
author("Kevin K. <kbknapp@gmail.com>"),
about("Does awesome things"),
arg("config").~> (
short("c"),
value_name("FILE"),
help("Sets a custom config file"),
takes_value(true),
),
arg("INPUT").~> (
help("Sets the input file to use"),
required(true),
index(1),
),
subcommand("test").~> (
about("controls testing features"),
author("Someone E. <someone_else@other.com>"),
arg("debug").~> (
short("d"),
help("print debug information verbosely"),
),
),
).get_matches();
Summary
- Don’t looks that bad
- Introduces
three edit: two operators, however their usage is very intuitive
- Fixes some existed problems and reduces boilerplate
- Introduces breaking change
- Promotes different programming style