Function pointer to function that returns function pointer

I'm writing a simple text-based "choose your own adventure" game. Initially I had code like this:

    fn page1() {
        if player_chooses_to_go_west {
            page2();
        } else {
            page3();
        }
    }
    fn page2() { todo!() }
    fn page3() { todo!() }
    fn main() { page1(); }

but this is no good, since the call stack will grow indefinitely as the game progresses.

Next attempt is to have the current state of the game represented by a function pointer, and each function returns the next function pointer - like so:

    fn page1() -> fn() -> fn() {
        if player_chooses_to_go_left {
            page2
        } else {
            page3
        }
    }

    fn page2() -> fn() -> fn() { todo!() }
    fn page3() -> fn() -> fn() { todo!() }
    
    fn main() {
        let mut f = page1;
        loop {
            f = f();
        }
    }

This feels workable, except that I can't get the right return types for the functions. They just grow forever - fn(), fn() -> fn(), fn() -> fn() -> fn(), etc.!
(Changing to boxed closures doesn't help - the same problem emerges.)
It feels like this approach should work, since each function is ultimately just returning a thin pointer. But perhaps the type system won't allow it.
Is there a way that this approach can work?

For future reference, I got it working like the code below. It's ugly but it gets the job done. :slight_smile:

use std::any::Any;

fn page1() -> Box<dyn Any> {
    let player_chooses_to_go_left = true;
    let boxed_f: Box<fn() -> Box<dyn Any>> = if player_chooses_to_go_left {
        Box::new(page2)
    } else {
        Box::new(page3)
    };
    boxed_f
}

fn page2() -> Box<dyn Any> { todo!() }
fn page3() -> Box<dyn Any> { todo!() }

fn main() {
    let mut f: Box<fn() -> Box<dyn Any>> = Box::new(page1);
    loop {
        let next_f = f();
        f = next_f.downcast().unwrap();
    }
}

Don't know that it's the best design, but anyway:

3 Likes

I'm not the best Rust expert here, but wouldn't this be a case where making use of closures and trait bounds make more sense ? Something like this (please correct my code if you notice a mistake):

fn page<T>() -> &T
where
    T: Fn() -> &T,
{// TODO...}

(would something like that even compile ?)

That said, I don't exactly understand your design decision. What is the incentive to represent your pages as function pointers ? Correct me if I am wrong, but doesn't a Choose-Your-Own-Adventure game/visual novel work just like a state machine ? Wouldn't it be easier to implement your pages as the different cases of an Enum or with Structs and using functions to transition between pages (states) ?

A minimal change to fix the original code is to introduce a type for the functions returned:

struct Page(fn() -> Page);

fn start() -> Page {
    Page(page1)
}

fn page1() -> Page {
    Page(if goes_left() { page2 } else { page3 })
}

This is a general solution (in many programming languages, even) to an infinitely recursive type: adding a nominal type makes it workable, because recursive data structures are a feature in almost any language that has data structures, so the compiler has to support them, and adding a function instead of a direct value doesn't especially complicate the matter.

8 Likes

I like this, basically in each iteration of loop calling the same function but its actually just a renamed different function

The current simple design (functions returning function pointers) is almost certainly not what I'll end up with. I know it's going to get much more complicated - most likely the pages will be stored externally, for example. I just wanted to see if it could work.

TIL you can use Self in a recursive struct definition o.O

(I thought Self was for impl blocks only… well, and trait definitions, too…)

2 Likes

RFC 2300 stabilized in Rust 1.32... but it just seemed natural to me for whatever reason. [1] Sorta funny, there's a lot of other pre-NLL-or-so habits I haven't kicked.


  1. I thought maybe I picked it up from "too many lists", but a skim suggests not. ↩︎

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.