LINUX.ORG.RU

Rust, ассоциировать функцию со структурой

 


0

3

Предположим, что я хочу сделать «контейнер», который хранит дженерик данные и в определённые моменты «валидировать» их:

struct Container<Data, Validate: Fn(&Data) -> bool> { ... }
Хранить функцию в каждом экземпляре структуры, по идее, нет необходимости, тем более, что в рантайме она меняться не будет. Следовательно хочется использовать PhantomData:
struct Container<Data, Validate: Fn(&Data) -> bool> {
    data: Vec<Data>,
    validate: PhantomData<Validate>,
}
Но дальше возникают вопросы.

Во первых, хочется иметь дефолтное значение, но написать struct Container<Validate: Fn(&Data) -> bool = foo> нельзя - ведь функция это значение, а не тип.

Во вторых, как это использовать? PhantomData только в качестве маркеров и пригодно?

В общем, можно ли как-то вообще извернуться или функцию всё-таки придётся хранить в объекте?

На плюсах (просьба не реагировать как на красную тряпку - просто для иллюстрации) это могло бы выглядеть как-то так:

bool foo(int) { return false; }

template <bool (*f)(int) = foo>
struct S {
    bool foo() { f(10); }
};

★★★★★

В общем, можно ли как-то вообще извернуться или функцию всё-таки придётся хранить в объекте?

Если ты собрался менять функцию в зависимости от ситуации, то да, т. к. это уже не известно во время компиляции.

Если же нет, то можно использовать трейт для конкретного типа данных.

trait Validate {
    fn validate(&self) -> bool;
}

struct Data;

impl Validate for Data {
    fn validate(&self) -> bool { true }
}

struct Container<T> {
    data: Vec<T>,
}

impl<T: Validate> Container<T> {
    fn validate_all(&self) -> bool {
        self.data.iter().all(Validate::validate)
    }
}
anonymous ()
Ответ на: комментарий от littlechris

не придётся.

Спасибо, я знаю, что в расте есть трейты. Не затруднит тебя показать что именно подразумевается? Можно псевдокодом.

DarkEld3r ★★★★★ ()
Ответ на: комментарий от anonymous

Если ты собрался менять функцию в зависимости от ситуации,

Нет, забыл это уточнить. Вернее, показалось, что понятно из условий.

Если же нет, то можно использовать трейт для конкретного типа данных.

Можно-то можно, но так мне придётся писать реализацию для разных типов. Неприятно тут ещё то, что трейт будет мой, а значит пользователи не смогут реализовывать его для других сторонних типов.

И что хуже - нельзя будет сделать несколько реализаций для одного и того же типа.

DarkEld3r ★★★★★ ()
Ответ на: комментарий от DarkEld3r

Можно-то можно, но так мне придётся писать реализацию для разных типов.

Так ведь у типов разное поведение, чтобы сделать одну функцию валидации, в неё по-любому надо передавать trait object fn validate(data: &Validate), который (трейт) приведёт разные типы к одному поведению.

Неприятно тут ещё то, что трейт будет мой, а значит пользователи не смогут реализовывать его для других сторонних типов.

Но можно реализовать трейт для своего типа.

И что хуже - нельзя будет сделать несколько реализаций для одного и того же типа.

Если стоит такое условие, то только по значению. Иначе прийдётся делать такие неприятные вещи.

struct OverrideValidate(Data);

impl Validate for OverrideValidate {
    fn validate(&self) -> bool {
        ...
    }
}
anonymous ()
Ответ на: комментарий от anonymous

Так ведь у типов разное поведение

Ну это от контекста зависит. Я-то как раз хотел сделать дженерик-реализацию, но дать возможность её заменять.

Если стоит такое условие, то только по значению.

Мне тут интересную штуку подсказали:

fn validate<Data>(_: &Data) -> bool {
    false
}

struct Container<Data, Validate: Fn(&Data) -> bool> {
    data: Vec<Data>,
    validate: Validate,
}

fn default_container<Data>() -> Container<Data, impl Fn(&Data) -> bool> {
    Container {
        data: Vec::new(),
        validate: validate,
    }
}
https://is.gd/Ku5wAQ

Правда не получится использовать (логичный) Default трейт.

DarkEld3r ★★★★★ ()
Последнее исправление: DarkEld3r (всего исправлений: 2)
Ответ на: комментарий от tailgunner

Ты хочешь параметризовать дженерик объектом? В Rust это невозможно.

Во первых, не совсем понимаю, что значит «параметризовать дженерик объектом».

struct O {}
struct G<T = O> { ... }
Разве вот это не оно?

Во вторых, мне, в первую очередь, хотелось «параметризовать поведение» и не хранить в объекте лишнего, если это возможно. И это возможно, даже без impl trait:

trait Validator<Data> {
    fn validate(_: &Data) -> bool;
}

struct DefaultValidator {}

impl<Data> Validator<Data> for DefaultValidator {
    fn validate(_: &Data) -> bool {
        false
    }
}

impl Default for DefaultValidator {
    fn default() -> Self {
        DefaultValidator {}
    }
}

struct Container<Data, V: Validator<Data> + Default = DefaultValidator> {
    data: Vec<Data>,
    validator: V,
}

impl<Data> Container<Data, DefaultValidator> {
    fn new() -> Self {
        Container {
            data: Vec::new(),
            validator: DefaultValidator::default(),
        }
    }
}
Благодаря тому, что пустые объекты в расте занимают нулевой объём, размер контейнера не увеличится. Получается даже гибче чем в плюсах, где можно или указателем на функцию параметризировать, но тогда объект уже не подсунуть или хранить std::function, но тогда размер объекта будет больше, даже если ничего хранить не надо.

Можно даже мою изначальную хотелку с подсовыванием функции сделать, если реализовать Fn для Validator.

И «бонусный вопрос»: может в курсе почему impl trait решили делать именно в таком виде? Разве не было бы логичнее делать аналогично с входными параметрами?

fn foo(Input: SomeTrait, Output: SomeOtherTrait)(val: &Input) -> Output { ... }

DarkEld3r ★★★★★ ()
Последнее исправление: DarkEld3r (всего исправлений: 2)
Ответ на: комментарий от DarkEld3r

Разве не было бы логичнее делать аналогично с входными параметрами?

Нет. Параметры дженерика - это то, что можно менять снаружи. Конкретный тип impl Trait'а нельзя менять снаружи - он выводится в зависимости от внутренностей функции. Так что, не вижу в этом никакой нелогичности.

Esper ()
Ответ на: комментарий от Esper

Конкретный тип impl Trait'а нельзя менять снаружи - он выводится в зависимости от внутренностей функции.

Да, логично, это мне в голову не приходило.

DarkEld3r ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.