1

Being an aspiring rustacean, I've been working my way through The Rust Programming Language book and being in the 13th chapter I was attempting to generalize the Cacher struct, that has as a purpose implementing lazy evaluation around a closure. While I was able to use generics to generalize the closure signature to any one parameter with any one output type, I can't figure out how to generalize this to closures with any number of params. I feel like there should be a way to do this.

struct Cacher<'a, Args, V: Clone>  
{
    calculation: &'a dyn Fn(Args) -> V,
    value: Option<V>
}

impl<'a, Args, V: Clone> Cacher<'a, Args, V>
{
    fn new(calculation: &'a dyn Fn(Args) -> V) -> Cacher<Args, V> {
        Cacher {
            calculation: calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: Args) -> V {
        // all this cloning is probably not the best way to do this
        match self.value.clone() {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v.clone());
                v
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let mut cached_func = Cacher::new(&(|asd| asd + 1));
        assert_eq!(cached_func.value(1), 2);
        assert_eq!(cached_func.value(4), 2);
    }

    #[test]
    fn it_works_too() {
        // compiler hates this
        let mut cached_func = Cacher::new(&(|asd, qwe| asd + qwe));
        assert_eq!(cached_func.value(1, 1), 2);
        assert_eq!(cached_func.value(4, 1), 2);
    }
}

Tomás Mena
  • 208
  • 2
  • 7

3 Answers3

2

You can do this on nightly using the fn_traits (and closely related unboxed_closures) features. This allows you to use Fn like Fn<Args, Output = V> where Args is a tuple type of all the parameters passed to the function.

#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct Cacher<'a, Args, V: Clone>  
{
    calculation: &'a dyn Fn<Args, Output = V>,
    value: Option<V>
}

impl<'a, Args, V: Clone> Cacher<'a, Args, V>
{
    fn new(calculation: &'a dyn Fn<Args, Output = V>) -> Cacher<Args, V> {
        Cacher {
            calculation: calculation,
            value: None,
        }
    }

    fn value(&mut self, args: Args) -> V {
        // all this cloning is probably not the best way to do this
        match self.value.clone() {
            Some(v) => v,
            None => {
                let v = self.calculation.call(args);
                self.value = Some(v.clone());
                v
            }
        }
    }
}

This does require you to call value() with a tuple:

let mut cache1 = Cacher::new(&|a| a + 1);
let value1 = cache1.value((7,));

let mut cache2 = Cacher::new(&|a, b| a + b);
let value2 = cache2.value((7, 8));

However, you can make it nicer to use if you're willing to make the boilerplate for the numerous tuple types:

impl<'a, T, V: Clone> Cacher<'a, (T,), V>
{
    fn value2(&mut self, arg1: T) -> V {
        self.value((arg1, ))
    }
}

impl<'a, T, U, V: Clone> Cacher<'a, (T, U), V>
{
    fn value2(&mut self, arg1: T, arg2: U) -> V {
        self.value((arg1, arg2))
    }
}

// ...

let mut cache1 = Cacher::new(&|a: usize| a + 1);
let value1 = cache1.value2(7);

let mut cache2 = Cacher::new(&|a: usize, b: usize| a + b);
let value2 = cache2.value2(7, 8);

See it running on the playground.

This only works on nightly because its not yet been stabilized if this is how they will be supported generically in the future.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
1

In rust, functions do not have a variable numbers of arguments, except in some cases for compatibility with C. This answer provides more background.

In your example, you could achieve some generic lazy evaluation with the lazy static crate. You don’t pass a closure to this crate, not explicitly at least. But you put the body of the closure in a variable that lazy static evaluates on first access (a bit like a closure taking () and whose result would be stored in Cacher, if you will).

Clément Joly
  • 858
  • 9
  • 27
  • Hey, thanks for the answer, but this is not really what I am asking. I don't intend for a function to take any number of parameters, but to define a generic for any function, regardless of it being a function that takes one parameter, two or any number. I will definitely take a look at the code for lazy static though, thank you! – Tomás Mena Apr 05 '21 at 17:03
  • I’m sorry I misunderstood your question. That’s clearer to me now, but I’ve nothing more to suggest yet. – Clément Joly Apr 05 '21 at 20:42
1

It's fairly hard to understand exactly what is it that you need. So here's my guess:

struct Cacher<'a, Args, V: Copy>
{
    calculation: &'a dyn Fn(Args) -> V,
    value: Option<V>
}

impl<'a, Args, V: Copy> Cacher<'a, Args, V>
{
    fn new(calculation: &'a dyn Fn(Args) -> V) -> Cacher<Args, V> {
        Cacher {
            calculation: calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: Args) -> V {
        // Cloning fixed
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let mut cached_func = Cacher::new(&(|asd| asd + 1));
        assert_eq!(cached_func.value(1), 2);
        assert_eq!(cached_func.value(4), 2);
    }

    #[test]
    fn it_works_too() {
        // The compiler is fine
        // Although now, it's not multiple arguments but rather one arg, acting as many
        let mut cached_func = Cacher::new(&(|asd: (usize, usize)| asd.0 + asd.1));
        assert_eq!(cached_func.value((1, 1)), 2);
        assert_eq!(cached_func.value((4, 1)), 2);
    }
}

Remember that Rust's generics could be considered as Algebraic Data Types, hence, only enums, structs and functions are allowed (closures too, if you consider them different to functions). The second test works because tuples could be considered structs.

Because of this, it's impossible to have multiple arguments in one function definition.

The usual way that rust solves this issue is with macros. Although method macros don't exist in rust yet.