Method-cascading and pipe-forward operators proposal

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 functions on value and ignore result of them
  • mutable.~>(function(,),) - if one or more of functions needs value to be bound as mut

where trailing comma means that other functions/arguments might be applied.

Let analyze all parts:

  1. ~ has the same meaning as in previous section - function mutates. Also nicely combines with > into ~> arrow.
  2. ~> and > neatly shows direction where value is passed.
  3. . 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).
  4. ( and ) shows that we have limited scope where we can operate on value.
  5. ( and ) are chosen over { and } to not confuse that syntax with expressions and to not add new symbols in call chain syntax.
  6. , 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:

  1. Functional programmers uses it extensively, modern languages have it in arsenal, it helps, and not providing it in Rust is rather restrictive.
  2. 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.
  3. 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.
  4. 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:

  1. Error handling from external functions: mapping, unwrapping, using ?
  2. 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

  1. Don’t looks that bad
  2. Introduces three edit: two operators, however their usage is very intuitive
  3. Fixes some existed problems and reduces boilerplate
  4. Introduces breaking change
  5. Promotes different programming style
1 Like