Add Kotlin Standard Library's functions: let & also

To better support method chaining, Rust could include the following trait, which can improve the user experience:

pub trait KtStd {
    fn let_imut<R, F>(&self, block: F) -> R where F: FnOnce(&Self) -> R;
    fn let_mut<R, F>(&mut self, block: F) -> R where F: FnMut(&mut Self) -> R;
    fn let_owned<R, F>(self, block: F) -> R where Self: Sized, F: FnOnce(Self) -> R;

    fn also_imut<F>(&self, block: F) -> &Self where F: FnOnce(&Self);
    fn also_mut<F>(&mut self, block: F) -> &mut Self where F: FnMut(&mut Self);
}

Before it:

let mut s = String::new();
stdin().read_line(&mut s).unwrap();
s. // do something.
println!("{}" s);

After:

String::new()
    .also_mut(|mut s| { stdin().read_line(&mut s).unwrap(); }) // return &mut s
    . // do some Method Channing.
    .let_imut(|s| println!("{}", s)) // return closure result

Here is already an implementation:

impl<T> KtStd for T {
    fn let_imut<R, F>(&self, block: F) -> R
    where
        F: FnOnce(&Self) -> R,
    {
        block(self)
    }
    fn let_mut<R, F>(&mut self, mut block: F) -> R
    where
        F: FnMut(&mut Self) -> R,
    {
        block(self)
    }
    fn let_owned<R, F>(self, block: F) -> R
    where
        Self: Sized,
        F: FnOnce(Self) -> R,
    {
        block(self)
    }

    fn also_imut<F>(&self, block: F) -> &Self
    where
        F: FnOnce(&Self),
    {
        block(self);
        self
    }
    fn also_mut<F>(&mut self, mut block: F) -> &mut Self
    where
        F: FnMut(&mut Self),
    {
        block(self);
        self
    }
}
1 Like

How is this an improvement? The "after" snippet looks like it has exactly the same "real code" as the "before" snippet, except wrapped in closures.

14 Likes

Defining variables and moving the cursor frequently will interrupt the developer's thinking. If this feature is available, the developer's thinking will be natural and spontaneous.

1 Like

This seems very subjective. I'm with lxrec in that this is no improvement at all. It suddenly uses closures where none are needed. It's longer. It uses naming that's no where near common (imut). And additionally it doesn't allow anything that's not already possible (including users implementing this themselves if they feel the need for it)

14 Likes

I absolutely appreciate the motivation for this, and I've wanted a similar thing in the past. However, there's no need for this to go in the standard library; it could just as easily be an external crate. (You could even publish it yourself!)

As a more minor point, in the current implementation, the naming of the methods is inconsistent with Rust precedent and feels nonintuitive to me personally. But I'm sure it would be possible to find different names that would improve upon that.

10 Likes

let_ref would be more idiomatically consistent. And I agree this can be a crate -- it's a bit like tap.

4 Likes

tap already exists for fn TapOos::tap<R>(self, f: impl FnOnce(&mut Self) -> R)) -> Self. I've used it a few times before.

This is actually also enough for the by-ref case: since all T: Sized impl TapOps, you can <&_>::tap for the by-ref case.

Strangely enough, I actually think this functionality might be able to fit in std. Why? It's in a similar situation to dbg!: it's the kind of tiny helper that you might use and enjoy using if you have it, but you're unlikely to reach to a crate to provide it, because just doing it without a crate and a simple scope is just as easy.

Though it really isn't as useful as in Kotlin: you still have to come up with a name to be used in the closure, so you aren't saving a name. (In Kotlin, .let/.also provide access as the receiver self or the unnamed closure arg it, though I don't recall which is which.)

However, adding a method to all types is an invasive change for the std lib prelude. Because of that, it'd have to be a non-prelude trait. But at that point, there's little reason anymore to have this in std rather than a crate.

12 Likes

I do like concept, I sometimes have to break up a chain because one of the functions I want to invoke doesn't return self, although if postfix macros ever materialize this could possibly become a macro. Which would remove the need to impl a trait on every type.

String::new()
  .then!(mut s, stdin().read_line(&mut s).unwrap())
  .then!(s, println!("{}", s))
4 Likes

How are these 2 snippets semantically different?

String::new()
  .then!(mut s, stdin().read_line(&mut s).unwrap())
  .then!(s, println!("{}", s))
let mut s: String::new();
stdin().read_line(&mut s).unwrap();
println!("{}", s);

I ask, because the 2 examples have an equal number of SLOC, and line-for-line seem to have identical semantics. So it doesn't cut boilerplate, and I'm not even sure it's easier to type. So what advantage does the 1st snippet have that I'm missing here? That it might be an expression*?

*I'm not sure if it would be an expression at all, since it looks like the macro might perhaps have some influence over that, depending on design and implementation.

1 Like

...surprisingly 1st indeed feels easier to grasp

1 Like

I agree with you. I was using the original example with the postfix idea for contrast.

My argument wasn't whether or not this pattern is useful in the original example. But that if the pattern was desired in some situation, then it could be handled using another potential feature already in discussion. Whether or not a then!() macro, or any other generic map!() or filter!() macro belongs in std is an argument I will defer.

2 Likes

Keep in mind that RFC 2442 (postfix macros) does not define typed postfix macros. .then!() may be generalizable, but the vast majority of postfix macros would not be. I'd love to see typed postfix macros as much as anyone else, but it would require a fundamental shift in how macros are processed.

2 Likes

This is an important difference to let_ref (renamed from imut for consistency) and let_mut helpers: these use anonymous / universal lifetimes for their closure, meaning that doing

[42, 27]
    .let_ref(|it| &it[0]) // HRTB lifetime gives `it` the usability (rather the lack thereof) of a local

would fail with a lifetime error.

The signatures for let_ref and let_mut need to be:

pub trait KtStd {
    // allow `R` to depend on `'a`
    fn let_ref<'a, R, F>(&'a self, then: F) -> R where F: FnOnce(&'a Self) -> R;
    fn let_mut<'a, R, F>(&'a mut self, then: F) -> R where F: FnMut(&'a mut Self) -> R;

Regarding the naming, with is more consistent for this style of patterns:

.with_ref(|it| ...) // sugar for `(&<expr>).with(|it| ...)`, much like `.iter()` _w.r.t._ `.into_iter()`
.with_mut(|it| ...) // ditto for `&mut` and `iter_mut()`
.with(|it| ...) // may be shadowed by inherent `with`s (_e.g._, `thread_local!`'s),
                // so a `with_owned` alias could be added to disambiguate.

Also, we'd need the try_with... fallible variants.

2 Likes

Relatedly, sometimes when working on the error message API in rustc, I find myself wanting something like:

self.struct_span_err(...).with(|err| {
    err.span_label(...)
       .span_label(...);
})

where with has the signature as fn with(mut self, modify: FnOnce(&mut Self)) -> Self.

The main point of this is that I have a builder taking in and returning &mut Self, but I don't want to give up access to the builder by-value.

1 Like