Method-cascading and pipe-forward operators proposal

Thank you all for replying and appreciating my work. That really motivates me to move further!

@Nokel81 I will start two separate RFCs where discussions will be more focused; this thread will be more about hacks around ~ and how they play together.

About inline closures (@ExpHP might be interested too): this is a way how Kotlin does pipe-forwarding.
I like it. However currently that might not be a best way to do things in Rust:

  • In Kotlin you can wrap everything in try-catch, add Throws annotation or just ignore error which you know never happens - Rust domain prohibits that, you must handle errors
  • Kotlin allows explicit this and don't requires you to declare closure argument (it will be named it by default which is great if you uses it moderately) - Rust prevents you to unintentionally crack someone's head with superpowers and requires to write everything explicitly
  • Lurking through backtrace of panic that occurred in deeply stacked closures might be not fun
  • You can write more complex code that it should be (sometimes I catch myself on that)

After all that listed I started thinking that the following code is more idiomatic than closures:

    return {
        let mut p = PathBuf::from("base");
        fs::create_dir_all(&p)?;
        p.push("filename");
        File::open(p);
    }

But closures might be a way to go after some ergomomic improvement.

And interesting idea arrived in my head, that even might be proposed alongside with previous two!
It:

  • allows closure to return its argument and ignore its expression result.
  • also uses ~ and has the same borrowing semantics.
  • works with Try trait to infer when argument should be returned wrapped in Ok( ):
  • can improve iterators and futures a bit

I've taken @ExpHP idea of Thru trait and here is example:

    return PathBuf::from("base")
        .thru(|~p| create_dir_all(&p))?    // `Ok(p)` is returned here.
        .thru(|~mut p| p.push("filename")) // `p` is returned here.
        .thru(File::open)

Don't looks that bad. But still has serious problem that I want to avoid:

You can write more complex code that it should be

It's like another dimension is added to text, and that makes it harder to reason about where to move. Nothing will prevent you to write more and express less (e.g. by nesting thru instead of flattening them, putting additional logic in closures where it shouldn't be, add complex error handling etc.). I think that this problem should be prevented and preventing problems by using effective design that dont introduces them is where Rust really shines.
That motivates me to defer solution and return to improving initial proposal:

    return first()
        .(second) // Methods that takes by value are unchanged.
        .~(third) // `~` is moved outside.
    return PathBuf::from("base")
        .~(fs::create_dir_all(&).unwrap()) // Allows to split flow!
        .~push("filename")                 // `.~` chain is now consistent.
        .(File::open)
    let content = String::new()
        .~(file.read_to_string(&mut)?);

Slightly better, however part that allows to split flow is very controversial even for me.


Need to mention that pipeline.rs crate exists.
It uses macros and might be interesting alternative, but I've don't tried it yet.


About performance what most interested me is that isn't such DSL more lightweight than closures / macros?
Is there any impact of Builders, especially when all Self-type-returning-checking logic runs?
Some time ago I've tried to measure that without success (probably because code was optimized) and more advanced knowledge was needed to do that properly.


@ExpHP, the following example shows what happens with method-cascading, #[must_use] and ?:

    file.~read_to_string(&mut buffer)?; // Produces warning without `?`

If type is other than Result, I think it shoudn't be discarded, and there will be no other way to flush warning than use return value.