How to properly use higher-order functions? (plus some strange behavior)


#1

Hi! I’m very, very new to Rust. I am currently messing with returning funcions (closures, really) and accepting functions as arguments. I haven’t systematically read any particular Rust tutorial, so i might be missing some simple knowledge. Haven’t even installed the complier yet, only been messing with the play.rust-lang.org site.

Right now, my self imposed task is (at least, a general idea of what am i aiming for):
Define a “map” function over a standard collection type (ideally arrays, since they seem simpler),
Create a function that returns a function,
Call this function and store its return value in a variable,
Create a value of said collection type,
Then finally, call my “map” with the collection and the returned function.

This ought to be a simple enough usage of the higher-order features of the language. It hasn’t been easy, so far.

Following is the code i have so far. There are two (identical) lines commented. The surprising behavior happens when you uncomment either one. At least on my setup, which is the play.rust-lang.org editor+compiler (nightly), the code only works when you uncomment the bottom one. I have no idea why, and would be grateful if anyone could explain why.

#![feature(core, unboxed_closures)]

struct TS<A,B> {t: Box<Fn(A) -> B>}
impl<A,B> FnOnce<(A,)> for TS<A,B> {
    type Output = B;
    extern "rust-call" fn call_once(self, args: (A,)) -> B {
        (self.t)(args.0)
    }
}
impl<A,B> FnMut<(A,)> for TS<A,B> {
    extern "rust-call" fn call_mut(&mut self, args: (A,)) -> B {
        (self.t)(args.0)
    }
}
impl<A,B> Fn<(A,)> for TS<A,B> {
    extern "rust-call" fn call(&self, args: (A,)) -> B {
        (self.t)(args.0)
    }
}

fn map<A,B,F>(vec_orig : &Vec<A>, func : F)  -> Vec<B> 
  where F : Fn(&A) -> B {
    let mut vec_to_return = Vec::with_capacity(vec_orig.capacity());
    for orig_elem in vec_orig.iter() {
        vec_to_return.push(func(orig_elem));
    }
    vec_to_return
}

fn mult_by(n:i32) -> Box<(Fn(&i32) -> i32)> {
    Box::new(move |x| {x*n})
}

fn factory() -> Box<Fn(&i32) -> i32> {
    let num = 6;
    Box::new(move |x| x + num)
}

fn add_five(x:&i32) -> i32 {
    x + 5
}

fn main() {
    let f = add_five;
    let g = factory();
    let h = mult_by(7);
    
    //let i : TS<&i32,i32> = TS {t: factory()};        
    let four : i32 = 4;
    let five : i32 = 5;
    //let i : TS<&i32,i32> = TS {t: factory()};
    
    let vec_num = vec![1,2];
    let vec_num2 = map(&vec_num, f);
    
    let calc1 = i(&four);
    let calc2 = i(&five);
    
    println!("{}", calc1);
    println!("{}", calc2);
    
    println!("{}, {}", vec_num2[0], vec_num2[1]);
}

It goes without saying i’ve already tried other things here and there; for example, since you can call a boxed function as if there was no box, i’d expect to be able to pass the box straight into the map, alas it wasn’t meant to be, AFAIK.

And of course, what i really want with this code would be to replace, in the line “let vec_num2 = map(&vec_num, f);” , the usage of “f” by “i”. Unfortunately things got too confusing for me.


#2

First of all, the way you want to manipulate higher-functions is not idiomatic to Rust. Like Scala, Rust uses iterators to apply operations on a collection, so basically you can do: vec_num.iter().map(f).collect().

I’m not sure what is the purpose of TS but the first does not compile due to the reference &i32, if you change it for i32, it’ll work. The borrow checker tells you that "four does not live long enough" because i has type TS<&'a i32, i32> and &four has type &'b i32, the problem is that 'a lives longer than 'b and thus you can’t initialize this structure with &four. This is because, in the mind of the borrow checker, i could be used after that four has been destroyed. However the second commented line works because i will be destroyed before four and thus 'a < 'b.

IMHO, implementing FnOnce/Fn/FnMut is not common for Rust users, so please, stick at the idiomatic way of doing things :wink:


#3

Thanks for the heads-up on the idiom. This is but only a toy example made by a newbie; i had no intention of using this style in regular code. It just so happens that using plain functions is more convenient for a toy example.

The TS struct was merely a crutch i used to overcome a “limitation” of the trait system; i wanted to give an implementation of the FnOnce/Fn/FnMut traits for the Box<Fn(A)->B> type, but since neither the trait nor the type were defined in the module, i could not do so. Fortunately i remembered a trick from Haskell: you can just wrap the type with a “newtype” declarator in order to be able to implement another typeclass. In this case, the TS structure might be isomorphical to the underlying type, but since TS was defined in this module, the system accepts that i implement traits for it.

It was all confusing to me, because elements of Box<Fn(A)->B> are callable, and i had guessed that all callable objects in Rust already implemented one of FnOnce/Fn/FnMut, so i did feel like i was doing something wrong. So yeah, i did wish to avoid implementing those traits, but haven’t learned the idiomatic way to do so yet.

I think i get why the error happens. I haven’t yet read in detail about lifetimes, so i won’t bother you with my current model of how the concept works, but right now what looks surprising is the end of a lifetime. I had assumed that, if any two values were declared within the same “nesting level”, then they both were destroyed at the same time. If i get what you are saying, this is not quite the case.

In other words: You talk about four being destroyed while i is still alive. I can understand that if the code looked like this:

fn main() {
   ...
   let i = ...
   { let four = ...
   ...
   }
   // i is still alive, four is destroyed!
} // With the end of the function, i is also destroyed

But instead, my code looks like this:

fn main() {
   ...
   let i = ...
   let four = ...
   ...
   // Both are still alive, no?
} // Only here do the values get destroyed, right?

Would it be possible to destroy four (close its scope) before destroying i in the second example?

Oh, and on your proposed fix: Changing the type from &i32 to i32 seems out of the question. Part of my goal was to produce a function which i could pass into a “map” function. A “map” function, at least in my formulation, must create a new collection without altering the old one; therefore, the mapped function can only obtain a reference to elements of the old container. (Also worth remembering; the reason i wanted to have an object implementing Fn(&i32) -> i32 was so that i could pass it into the “map” function)

Thanks for the help so far!


#4

Lifetime are syntactically scoped (lives for an entire block {...}). Like in C++, there is a RAII mechanism using destructors (implemented by default in the Drop trait), and Drop is called in reverse order of declarations, that’s why the lifetime of four is shorter than the one of i, therefore i and four are not destroyed simultaneously.

The trick of wrapping object for implementing traits is well-known and valid in Rust. Anyways, in your case and as a beginner, I’d not suggest to implement Fn (and co.) traits since it’s a pretty advanced feature (and not necessary for many projects).

Finally, changing &i32 to i32 was not a fix, it was to emphasize that the reference was the problem.