Using enum variant as FnOnce in iced-rs

Watching a video tutorial about using iced-rs, I found an oddity in the usage of enum variants in place of FnOnce. At least, that's how I understand it. I suspect there is additional behaviour provided by the iced-rs, something like an extension trait, that I haven't discovered yet. Because I tried to reproduce such behavior in the Rust Playground and I received an (expected) compile-time error like "wrong type - FnOnce expected".

So, I have the following code:

#[derive(Debug, Clone)]
enum Message {
    Edit(text_editor::Action),
    FileOpened(Result<Arc<String>, ErrorKind>),
}

// skip

Command::perform(
    load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))),
    Message::FileOpened,
);

where the signature of Command::perform is:

pub fn perform<A>(
        future: impl Future<Output = T> + 'static + MaybeSend,
        f: impl FnOnce(T) -> A + 'static + MaybeSend,
) -> Command<A>

As I understand it, the second parameter must be a type of FnOnce. But why am I able to pass the Message::FileOpened variant instead of it? And why am I able to use Message::FileOpened without parameters, despite this variant having associated data (a tuple)?

Full source code

Show
use iced::widget::{column, container, horizontal_space, row, text, text_editor};
use iced::{executor, Application, Command, Length, Settings, Theme};
use std::io::ErrorKind;
use std::path::Path;
use std::sync::Arc;

fn main() -> iced::Result {
    Editor::run(Settings::default())
}

struct Editor {
    content: text_editor::Content,
}

#[derive(Debug, Clone)]
enum Message {
    Edit(text_editor::Action),
    FileOpened(Result<Arc<String>, ErrorKind>),
}

impl Application for Editor {
    type Executor = executor::Default;
    type Message = Message;
    type Theme = Theme;
    type Flags = ();

    fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
        (
            Self {
                content: text_editor::Content::new(),
            },
            Command::perform(
                load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))),
                Message::FileOpened,
            ),
        )
    }

    fn title(&self) -> String {
        String::from("Iced Editor")
    }

    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
        match message {
            Message::Edit(action) => self.content.edit(action),
            Message::FileOpened(result) => {
                if let Ok(content) = result {
                    self.content = text_editor::Content::with(&content)
                }
            }
        };

        Command::none()
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        let input = text_editor(&self.content).on_edit(Message::Edit);

        let position = {
            let (line, column) = self.content.cursor_position();

            text(format!("{}:{}", line, column))
        };

        let status_bar = row![horizontal_space(Length::Fill), position];

        container(column![input, status_bar].spacing(10))
            .padding(10)
            .into()
    }

    fn theme(&self) -> Theme {
        Theme::Dark
    }
}

async fn load_file(path: impl AsRef<Path>) -> Result<Arc<String>, ErrorKind> {
    tokio::fs::read_to_string(path)
        .await
        .map(Arc::new)
        .map_err(|error| error.kind())
}

You're not passing a variant value -- you're passing the variant's constructor, which is more-or-less a built in function.

let _: fn(Result<Arc<String>, ErrorKind>) -> Message = Message::FileOpened;

That's why you can do...

let _: Message = Message::FileOpened(Ok(Arc::new(String::new())));
// function call                    ^---------------------------^

You may see things like this in iterator adapaters and other mapping-like situations, since functions implement FnOnce.

let x = Some(String::new()); // `Option::<_>::Some` is also a constructor
let y = x.map(Arc::new); // `y: Option<Arc<String>>`

The second parameter must have a type that implement FnOnce (and the other specified bounds). impl Trait in argument position (APIT) is more-or-less the same as a generic type parameter with bounds.

    pub fn perform<T>(
        future: impl Future<Output = T> + 'static + MaybeSend,
        f: impl FnOnce(T) -> A + 'static,
    ) -> Command<A> {
    pub fn perform<T, Fut, F>(future: Fut, f: F) -> Command<A>
    where
        Fut: Future<Output = T> + 'static + MaybeSend,
        F: FnOnce(T) -> A + 'static,
    {
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.