How can I use closures as a return type from functions in a type-safe way?

Context: I'm trying to learn Rust by attempting to port some code from Elm.

Elm has ML like Algebraic Data Types where one can say

type Foo = Foo (Int -> Maybe String)

What I'm trying to figure out is how to write Int -> Maybe String in rust so that I can use that type with closures.

Here is some naive code that doesn't work but shows my attempt at implementing the type

struct State {
    value: i64,
}

type Doer = fn(s: State) -> Option<String>;

fn generate_doer(v: i64) -> Doer {
    return |s: State| -> Option<String> {
        if v == s.value {
            return Some(String::from("All Good"));
        } else {
            return None;
        }
    };
}

Can this be made to work in rust? If yes, how does a working implementation looks?

In Rust, every closure has its own unique type, which is different from the type of any other closure in the program, even if they have the same signature. When the closure does not capture any environment, this unique type can be automatically converted into the function pointer type, which is the kind of type used for Doer, but your closure does capture the environment (specifically, the v variable).

One thing you can do is the following:

struct State {
    value: i64,
}

fn generate_doer(v: i64) -> impl Fn(State) -> Option<String> {
    move |s: State| -> Option<String> {
        if v == s.value {
            return Some(String::from("All Good"));
        } else {
            return None;
        }
    }
}

Here, the impl Trait syntax is a way to say "this function returns some type that implements this trait" without saying what the type actually is. In this case, the compiler would replace it with the anonymous type that the closure is given at compile time.

However since impl Trait is not a specific type, but a placeholder that is replaced with one of many types at compile time, you can't use it outside of function signatures. Struct fields require an actual specific type.

If you need an actual specific type, you can instead do this:

struct State {
    value: i64,
}

type Doer = Box<dyn Fn(State) -> Option<String>>;

fn generate_doer(v: i64) -> Doer {
    Box::new(move |s: State| -> Option<String> {
        if v == s.value {
            return Some(String::from("All Good"));
        } else {
            return None;
        }
    })
}

An dyn Trait is a specific type called a trait object that any implementer of the trait can be converted into. So here, we first create a value of the closure's anonymous type, then box it, and then convert the box into the nameable type using dyn Trait.

It is worth mentioning that what you are doing in Elm is equivalent to the Box solution here. Elm has no equivalent of impl Trait.

7 Likes

Thank you for helping me cross that bridge. Now I've run into lifetime errors and move errors:

New code is:

struct State<C> {
    value: String,
    context: C,
}

type Doer<C, X> = Box<dyn Fn(State<C>) -> Option<X>>;

pub enum Foo<X> {
    Foo(String, X),
}

fn generate_doer<C, X>(f: Foo<X>) -> Doer<C, X> {
    Box::new(move |s: State<C>| -> Option<X> {
        match f {
            Foo::Foo(str, x) => {
                if str == s.value {
                    return Some(x);
                } else {
                    return None;
                }
            }
        }
    })
}

I do not understand why is X not living long enough. The IDE suggests adding : 'static and after I do that I ran into problems with String not implementing Copy.

Box has an implied 'static bound, which means it doesn't want any temporary references stored inside. X can be absolutely anything, including being a temporary reference. X: 'static prevents X from being a temporary reference. Alternatively, you can have X: 'a and Box<dyn Fn… + 'a> to allow Box to be tied to a smaller scope.

You have another problem:

Fn closure can be called many times. X is not copyable, so you have one non-copyable non-shareable instance of X, but promise to return it unlimited number of times. You're going to need either FnOnce or make X be Arc<X>, or Option<X> and take() it, or Copy/Clone it.

3 Likes