Is this idiomatic rust ??

I often find myself having to rewrite some code due to missing a 'let' function like in Kotlin.
for example:

let msg2 = protocol::LimitOrder::new_from_json(
    &String::from_utf8(message.data.clone()).unwrap_or_default(),
)
.and_then(|lo| lo.serialize());

where as a reader I first need to read the second line then the first to understand what is happening.

To fix this issue I created this small trait to make it more readable

trait ScopedFn<T> {
    fn then<A, F: FnOnce(T) -> A>(self, f: F) -> A;
}

impl<T> ScopedFn<T> for T {
    fn then<A, F: FnOnce(T) -> A>(self, f: F) -> A {
        f(self)
    }
}

let msg1 = String::from_utf8(message.data.clone()).unwrap_or_default()
    .then(|s| protocol::LimitOrder::new_from_json(&s))
    .and_then(|lo| lo.serialize());

Is this idiomatic rust?
Does this exist somewhere in the std lib?
Are there any downsides to this?

You might be interested in https://crates.io/crates/tap, particularly .pipe.

very cool, thanks!

tap and pipe in the crate is kinda like

trait ScopedFn<T> {
    fn then<A, F: FnOnce(T) -> A>(self, f: F) -> A;
    fn apply<A, F: FnOnce(&mut T) -> A>(&mut self, f: F) -> A;
}

impl<T> ScopedFn<T> for T {
    fn then<A, F: FnOnce(T) -> A>(self, f: F) -> A {
        f(self)
    }
    fn apply<A, F: FnOnce(&mut T) -> A>(&mut self, f: F) -> A {
        f(self)
    }
}

If it were me, I'd use ? and spread it out over three lines.

let json = std::str::from_utf8(&message.data)?;
let limit_order = LimitOrder::new_from_json(json)?;
let msg = limit_order.serialize()?;

In my experience, higher order functions tend to reduce readability because you have the extra indentation and added parentheses or |lo| syntax. Using ? let's you write more straight-line code.

10 Likes

good advice. the problem with the style of nested function calls is the data flows inward out, the "pipe" style or simply binding intermediate values to named variables make the code and data flow in the same direction which is generally easier to read and understand.

I just want to add, some people coming from other languages tends to nest function calls simply to avoid binding intermediate values to names out of habit.

for example, most other languages don't allow or at least warn about variable shadowing, so people decide to write code in this style (pseudo code):

var my_foo =
        validate_foo(
            deserialize_foo(
                load_saved_foo("foo.json")
            )
        );

just because otherwise they are forced to name things, which they don't want:

var foo_tmp1 = load_saved_foo("foo.json");
var foo_tmp2 = deserialize_foo(foo_tmp1);
var foo = validate_foo(foo_tmp2);

in rust, thanks to the language's type system and compiler's analysis capability, you have the freedom to write:

let foo = load("foo.json");
let foo = deserialize(foo);
let foo = validate(foo);

of course I'm not saying this style is better than alternatives. I just say rust can change the way to write code due to seemingly insignificant small details.

combine with the extension trait mechanism (write your own specialized tap!), you can wrap these functions into associated functions (i.e. "methods") and turn it into the "pipe" style:

let foo = load("foo.json")
            .deserialize()
            .validate();

but this needs significant more work, I would just use tap most of time.

3 Likes