0

The following compiles:

pub fn build_proverb(list: &[&str]) -> String {
    if list.is_empty() {
        return String::new();
    }
    let mut result = (0..list.len() - 1)
        .map(|i| format!("For want of a {} the {} was lost.", list[i], list[i + 1]))
        .collect::<Vec<String>>();
    result.push(format!("And all for the want of a {}.", list[0]));
    result.join("\n")
}

The following does not (see Playground):

pub fn build_proverb(list: &[&str]) -> String {
    if list.is_empty() {
        return String::new();
    }
    let mut result = (0..list.len() - 1)
        .map(|i| format!("For want of a {} the {} was lost.", list[i], list[i + 1]))
        .collect::<Vec<String>>()
        .push(format!("And all for the want of a {}.", list[0]))
        .join("\n");
    result
}

The compiler tells me

error[E0599]: no method named `join` found for type `()` in the current scope
 --> src/lib.rs:9:10
  |
9 |         .join("\n");
  |          ^^^^

I get the same type of error if I try to compose just with push.

What I would expect is that collect returns B, aka Vec<String>. Vec is not (), and Vec of course has the methods I want to include in the list of composed functions.

Why can't I compose these functions? The explanation might include describing the "magic" of terminating the expression after collect() to get the compiler to instantiate the Vec in a way that does not happen when I compose with push etc.

Edmund's Echo
  • 766
  • 8
  • 15

2 Answers2

5

If you read the documentation for Vec::push and look at the signature of the method, you will learn that it does not return the Vec:

pub fn push(&mut self, value: T)

Since there is no explicit return type, the return type is the unit type (). There is no method called join on (). You will need to write your code in multiple lines.

See also:


I'd write this more functionally:

use itertools::Itertools; // 0.8.0

pub fn build_proverb(list: &[&str]) -> String {
    let last = list
        .get(0)
        .map(|d| format!("And all for the want of a {}.", d));

    list.windows(2)
        .map(|d| format!("For want of a {} the {} was lost.", d[0], d[1]))
        .chain(last)
        .join("\n")
}

fn main() {
    println!("{}", build_proverb(&["nail", "shoe"]));
}

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Thank you. If `result.push(...)` works, what is preventing me from substituting `result` with the product of the expression that builds the `Vec`? The answer is exactly as @stargateur and your answer addresses. However, is it perhaps less about `push` not returning a `Vec`, but rather, that `push` requires a mutable reference to the `Vec`? All this to say, perhaps there is a shift that I need to pay attention to when composing functions: functions that take `Iterable` as a parameter, and those aren't traits, but struct-specific methods like `push`. Is that right? – Edmund's Echo May 29 '19 at 16:12
  • 1
    @Edmund'sEcho well you _could_ do [that](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6f60c94f78e1bda716b3c748dbea0412) – Stargateur May 29 '19 at 16:35
  • Fantastic. Thank you! I have more work to do on how I think about this. – Edmund's Echo May 29 '19 at 16:56
  • How my FP went wrong: To compose functions as I was, they need to be "pure"; "pure" allows for mutations, we just need referential transparency. I went astray by relating the `result` in `result.push(...)` as "one in the same" with the `result` in `let mut result =...`. The flaw: in `result.push(...)`, `push` looks "pure" from the input perspective (`(self, newValue)`) but is in fact "impure" *only* because it returns the same output no matter the input. So, I can't compose with `push`. Confounding my confusion, building a mutable reference with the intent of having it point to "not ()". – Edmund's Echo May 30 '19 at 14:06
  • @Edmund'sEcho _"`push` looks "pure" from the input perspective `((self, newValue))`"_ The effective input perspective here is `(&mut self, newValue)`, which clearly shows that it can modify `self`, and so is very likely not to be pure. – E_net4 May 30 '19 at 14:08
  • @E_net4 I think that is one of the misunderstandings about "pure". A state machine can be pure. All that pure requires is that the output be the same for the same input. All `push` had to do to become pure, is return `self`. I was stuck on "it was surely pure" because the it had the right inputs. I kept denying the reality of it's return value. Do you know what I mean? – Edmund's Echo May 30 '19 at 14:29
  • Sure. And one can provide [an implementation of such a method](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6bfa45d4cfb4e3e230d36fbe426610cd). What we generally don't seem to understand is why you overlooked the method's return type. – E_net4 May 30 '19 at 14:38
  • @E-net4 "why you overlooked the method's return type": My brain refused to see because I was stuck: why can't I substitute the `result` in `result.push(..)` with the code I was using to produce the `result` used on the right hand side of `let mut result =...`. I could not rationalize how they were different. *I now see* how `push` is a *method* in a way that is different than a *trait* can also be considered a method (they both have access to `self`). I kept thinking that `push` was a *trait*; that matters because traits don't normally (ever?) return `()`; instead, output depends on input. – Edmund's Echo May 30 '19 at 19:10
0

Thank you to everyone for the useful interactions. Everything stated in the previous response is precisely correct. And, there is a bigger picture as I'm learning Rust.

Coming from Haskell (with C training years ago), I bumped into the OO method chaining approach that uses a pointer to chain between method calls; no need for pure functions (i.e., what I was doing with let mut result = ..., which was then used/required to change the value of the Vec using push in result.push(...)). What I believe is a more general observation is that, in OO, it is "aok" to return unit because method chaining does not require a return value.

The custom code below defines push as a trait; it uses the same inputs as the "OO" push, but returns the updated self. Perhaps only as a side comment, this makes the function pure (output depends on input) but in practice, means the push defined as a trait enables the FP composition of functions I had come to expect was a norm (fair enough I thought at first given how much Rust borrows from Haskell).

What I was trying to accomplish, and at the heart of the question, is captured by the code solution that @Stargateur, @E_net4 and @Shepmaster put forward. With only the smallest edits is as follows: (see playground)

pub fn build_proverb(list: &[&str]) -> String {
    if list.is_empty() {
        return String::new();
    }
    list.windows(2)
        .map(|d| format!("For want of a {} the {} was lost.", d[0], d[1]))
        .collect::<Vec<_>>()
        .push(format!("And all for the want of a {}.", list[0]))
        .join("\n")
}

The solution requires that I define push as a trait that return the self, type Vec in this instance.

trait MyPush<T> {
    fn push(self, x: T) -> Vec<T>;
}

impl<T> MyPush<T> for Vec<T> {
    fn push(mut self, x: T) -> Vec<T> {
        Vec::push(&mut self, x);
        self
    }
}

Final observation, in surveying many of the Rust traits, I could not find a trait function that returns () (modulo e.g., Write that returns Result ()).

This contrasts with what I learned here to expect with struct and enum methods. Both traits and the OO methods have access to self and thus have each been described as "methods", but there seems to be an inherent difference worth noting: OO methods use a reference to enable sequentially changing self, FP traits (if you will) uses function composition that relies on the use of "pure", state-changing functions to accomplish the same (:: (self, newValue) -> self).

Perhaps as an aside, where Haskell achieves referential transparency in this situation by creating a new copy (modulo behind the scenes optimizations), Rust seems to accomplish something similar in the custom trait code by managing ownership (transferred to the trait function, and handed back by returning self).

A final piece to the "composing functions" puzzle: For composition to work, the output of one function needs to have the type required for the input of the next function. join worked both when I was passing it a value, and when I was passing it a reference (true with types that implement IntoIterator). So join seems to have the capacity to work in both the method chaining and function composition styles of programming.

Is this distinction between OO methods that don't rely on a return value and traits generally true in Rust? It seems to be the case "here and there". Case in point, in contrast to push where the line is clear, join seems to be on its way to being part of the standard library defined as both a method for SliceConcatExt and a trait function for SliceConcatExt (see rust src and the rust-lang issue discussion). The next question, would unifying the approaches in the standard library be consistent with the Rust design philosophy? (pay only for what you use, safe, performant, expressive and a joy to use)

Edmund's Echo
  • 766
  • 8
  • 15