This is an automated archive made by the Lemmit Bot.

The original was posted on /r/rust by /u/nikitarevenco on 2025-05-07 18:19:44+00:00.

Original Title: I’m using Iced for my screenshot app. It is a native UI library for Rust and I love it. One of the recent additions is “time-travel debugging” which completely blew my mind. It’s a great showcase for what functional, pure UI can accomplish. But can the Rust compiler help enforce this pureness?


I’m using iced, a native UI library for Rust inspired by Elm architecture (which is a purely functional way of doing UI) for my app ferrishot (a desktop screenshot app inspired by flameshot)

I recently came across a PR by the maintainer of iced which introduces “Time Travel Debugging”.

Essentially, in iced there is only 1 enum, a Message which is responsible for mutating your application state. There is only 1 place which receives this Message, the update method of your app. No other place can ever access &mut App.

This way of doing UI makes it highly effective to reason about your app. Because only Message can mutate the state, if you assemble all of the Messages you receives throughout 1 instance of the app into a Vec<(Instant, Message)>, (where Instant is when the Message happened).

You have a complete 4-dimensional control over your app. You are able to go to any point of its existance. And view the entire state of the app. Rewind, go back into the future etc. It’s crazy powerful!

This great power comes at a little cost. To properly work, the update method (which receives Message and &mut App) must be pure. It should not do any IO, like reading from a file. Instead, iced has a Task structure which the update method returns. Signature:

fn update(&mut App, Message) -> Task

Inside of this Task you are free to do whatever IO you want. But it must not happen directly inside of the update. Lets say your app wants to read from a file and store the contents.

This is the, impure way to achieve that by directly reading in the update method:

struct App {
 file\_contents: String
}

enum Message {
 ReadFromFile(PathBuf),
}

fn update(app: &mut App, message: Message) -> Task {

match message {

Message::ReadFromFile(file) => {

    let contents = fs::read_to_string(file);

    app.file_contents = contents;
}

}

Task::none()


}

With the above, time-travelling will not work properly. Because when you re-play the sent Message, it will read from the file again. Who’s contents could have changed in-between reads

By moving the impure IO stuff into a Task, we fix the above problem:

struct App {
 file\_contents: String
}

enum Message {
 ReadFromFile(PathBuf),

UpdateFileContents(String)


}

fn update(app: &mut App, message: Message) -> Task {

match message {

Message::ReadFromFile(file) => {

    Task::future(async move { 

        let contents = fs::read_to_string(file);

        // below message will be sent to the `update`

        Message::UpdateFileContents(contents)
    })
}

Message::UpdateFileContents(contents) => {
    app.file_contents = contents;

    Task::none()
}

}


}

Here, our timeline will include 2 Messages. Even if the contents of the file changes, the Message will not and we can now safely time-travel.

What I’d like to do, is enforce that the update method must be pure at compile time. It should be easy to do that in a pure language like elm or Haskell who has the IO monad. However, I don’t think Rust can do this (I’d love to be proven wrong).