Pull to refresh

Comments 22

Спасибо за статью, очень познавательно!
У меня появилось 2 вопроса.


  1. 'static-типы — здесь идет речь про lifetime, или же это какое-то отдельное понятие? Значит ли это ограничение, что все дерево должно быть определено уже на этапе компиляции?
  2. У меня сложилось впечатление, что если моделью захочется оперировать несколькими разными функциями, то и придется создавать несколько разных полей в структуре Component. Или с данным подходом можно будет менять ссылку с одного замыкания на другое? Меня настораживает, что если я захочу усложнить логику приложения, расширяя список функций, которыми я смогу взаимодействовать с моделью, то мне придется каждый раз менять объявление структуры компонента, добавляя в нее новые поля для замыканий.
UFO just landed and posted this here

Все же думаю, что статика никакой роли на выделение переменных на стеке не влияет. Насколько я понимаю, 'static в расте означает то, что не только размер, но еще и расположение значения в памяти будет уже известно на этапе компиляции. В куче можно разместить что-либо, лишь положив это в Box, Rc либо какой-то другой смарт-указатель. Меня просто немного сбила с толку формулировка "static-типы", но вижу, что и в коде используется 'static в качетсве lifetime bound, что как бы намекает… Последний уточняющий вопрос: значит ли данное ограничение, что составить дерево из произвольного количества компонент на основе, например, пользовательского input'а с таким подходом будет невозможно?


Кстати, енамы со значениями так же будут иметь известный размер, так как по факту это обычные union-ы, размер которых принимает наибольшее значение размера его варианта (+ тег).

На первые вопросы ответил ниже


значит ли данное ограничение, что составить дерево из произвольного количества компонент на основе, например, пользовательского input'а с таким подходом будет невозможно?

Нет, не значит )

Спасибо большое за ответы. Вроде бы все прояснилось теперь :)
Понял еще, что несколько путал эффекты от статических переменных (static), 'static -типов и 'static в качестве времени жизни — это создавало больше всего конфуза в понимании кода

Ну, 'static-типы — это такие типы, определение которых не содержит ссылок или содержит только статические ссылки (с лайфтаймом 'static). Да, это касается только лайфтаймов, никаких ограничений на задание самих значений полей нет.


В простейшем случае добавляется по одному полю-указателю на каждый метод, который должен оперировать "захваченным" типом. Но можно сделать и по-другому, например, так:


struct Component {
    node: Box<dyn Any>,
    model: Box<dyn Any>,
    method_closure: fn(&Component, MethodArgs) -> MethodResult,
}

enum MethodArgs {
    UseModelA,
    UseModelB(i32),
}

enum MethodResult {
    UseModelA(()),
    UseModelB(i32),
}

impl Component {
    fn new<Model: 'static + Debug>(node: Node<Model>, model: Model) -> Self {
        let method_closure = |comp: &Component, args: MethodArgs| {
            match args {
                MethodArgs::UseModelA => MethodResult::UseModelA(
                    comp.use_model_a_impl::<Model>()
                ),
                MethodArgs::UseModelB(x) => MethodResult::UseModelB(
                    comp.use_model_b_impl::<Model>(x)
                ),
            }
        };

        Self {
            node: Box::new(node),
            model: Box::new(model),
            method_closure,
        }
    }

    fn use_model_a(&self) {
        (self.method_closure)(self, MethodArgs::UseModelA);
    }

    fn use_model_b(&self, x: i32) -> i32 {
        match (self.method_closure)(self, MethodArgs::UseModelB(x)) {
            MethodResult::UseModelB(x) => x,
            _ => panic!("Wrong method_closure response type"),
        }
    }

    fn use_model_a_impl<Model: 'static + Debug>(&self) {
        let model = self.model.downcast_ref::<Model>().unwrap();
        println!("Model: {:?}", model);
        self.node.downcast_ref::<Node<Model>>().unwrap().use_model();
    }

    fn use_model_b_impl<Model: 'static>(&self, x: i32) -> i32 {
        self.model.downcast_ref::<Model>().unwrap();
        x
    }
}

Запустить

Вариант с вектором методов:


struct Component {
    node: Box<dyn Any>,
    model: Box<dyn Any>,
    methods: Vec<Box<dyn Any>>,
}

impl Component {
    fn new<Model: 'static + Debug>(node: Node<Model>, model: Model) -> Self {
        let use_model_a: fn(&Component) = |comp: &Component| {
            let model = comp.model.downcast_ref::<Model>().unwrap();
            println!("Model: {:?}", model);
            comp.node.downcast_ref::<Node<Model>>().unwrap().use_model();
        };

        let use_model_b: fn(&Component, i32) -> i32 = |comp: &Component, x: i32| {
            comp.model.downcast_ref::<Model>().unwrap();
            x
        };

        Self {
            node: Box::new(node),
            model: Box::new(model),
            methods: vec![Box::new(use_model_a), Box::new(use_model_b)],
        }
    }

    fn use_model_a(&self) {
        (self.methods[0]
            .downcast_ref::<fn(&Component)>()
            .unwrap()
        )(self)
    }

    fn use_model_b(&self, x: i32) -> i32 {
        (self.methods[1]
            .downcast_ref::<fn(&Component, i32) -> i32>()
            .unwrap()
        )(self, x)
    }
}

Запустить


Можно даже кастить функциональные указатели напрямую. Они одного размера, так что по идее с этим не должно быть проблем, хотя я на 100% не уверен в безопасности такого кода.

Возможно, мысль несколько мимо темы, но как было бы здорово, если бы типы тоже были first class citizens, как функции; т.е. чтобы была встроенная в язык возможность хранить тип в рантайме.
Даже не знаю, есть ли языки, где так можно делать?


Мне кажется, что довольно вещей можно было бы делать проще и чище.

Возможность хранить тип в рантайме — не нужна в 90% случаев, в остальных случаях применяется что-то вроде C++ного std::variant или C++ного-же RTTI (который я обычно первым делом отключаю)

UFO just landed and posted this here

Процитирую проблему из статьи:


Такое решение подходит для тех случаев, когда исходный тип известен в месте работы с ним. Но что делать, если это не так? Что делать, если вызывающий код просто не знает об исходном типе объекта в месте его использования? Тогда нам нужно как-то запомнить исходный тип, взять его там, где он определен, и сохранить наряду с типажом-объектом dyn Any, чтобы потом последний привести к исходному типу в нужном месте.

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


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


Представьте, что можно делать как-то так (прошу прощения для псевдо-С++):


class A { Type realType; };
class B : A
{

}

B b;
A * a = &b;
a.realType = typeof(b);

auto p = a as a.realType; // <<- auto выводит тип В
UFO just landed and posted this here

Я имел в виду, что вся статья вроде как решает именно эту проблему, просто окольными путями.


Хотя и тут можно сказать что если есть проблемы с дизайном, когда нужен даункастинг.

Это само собой, я не пытался продумать все нюансы, просто хотелось показать, как бы я хотел это видеть.


У Вас лично в практике была такая надобность? Можете описать?

Сходу что-то конкретное не могу вспомнить, но периодически натыкаюсь на необходимость "прыгать через обручи" — делать кучу специализаций шаблонов, потому что нельзя просто написать switch по типу (я понимаю, что это не рантайм, но синтаксис мог бы быть простым) или делать type-tag из enum'a руками, а потом свитчится по нему.


Я понимаю, что это не очень часто нужно и, наверное, затраты на разработку такой фичи того не стоят. Но каждый раз, когда приходится изобретать обходные пути, становится грустно.


Вот рефлексия, допустим, тоже не слишком-то часто нужна, но без нее тоже грустно (имхо это достаточно близкие вещи).

UFO just landed and posted this here

Сериализация-десериализация без рефлексии — это, как правило, боль и унижение. Хотя Rust справился.

UFO just landed and posted this here

Дело в том, что типы (по крайней мере в Rust) — это объекты времени компиляции. Тут нужен способ некоего "структурного запоминания", если таковой возможен.

Да, это понятно. Я просто мечтал о том, как могло бы быть.
Помнится, натыкался на пост о проблемах в D, где описания типов могут занимать дофигища места из-за декорирования имен. Во, нашел.


Create a chain of 12 calls to square above and the symbol length increases to 207,114. Even worse, the resulting object file for COFF/64-bit is larger than 15 MB and the time to compile increases from 0.1 seconds to about 1 minute. Most of that time is spent generating code for functions only used at compile time.

Я понимаю, что очень плохо представляю насколько все это сложно и не претендую на экспертное мнение. Просто вздыхаю.

В Rust по идее можно развить функционал ассоциированных типов для достижения нечто подобного. Собственно, первый вариант "замыкания" пытался их использовать, но ограничения системы типов текущего Rust не позволили.

К сожалению, я плохо понимаю теорию языков программирования, поэтому не знаю, почему нельзя просто сделать тип Type или вроде того.


Вроде бы в Питоне так можно. И в Руби вроде есть класс Class. Хм. Может, к этому больше тяготеют языки с динамической типизацией, потому что там люди вынуждены тип в рантайме проверять.

К этому тяготеют языки с динамической типизацией, потому что там разработчики языка вынуждены таскать вместе с объектом ссылки на его функции в том или ином виде из‐за этой самой динамической типизации. Т.к. наличия таких ссылок при динамической типизации не избежать, то нет особого смысла скрывать их от пользователя.


В языках со статической типизацией компилятор обычно знает, какие именно функции будут вызваны в любом месте программы (или хотя бы знает, что здесь происходит вызов функции, адрес которой в runtime можно найти вот в этом месте памяти), поэтому ему совершенно не нужно тащить в runtime набор ссылок на функции, относящиеся к данному типу. Т.к. набор ссылок на функции для работы программы не нужен, а вот выкидывать неиспользованные функции как раз‐таки часто нужно во имя эффективности, то в языках со статической типизацией никто никаких Type не делает. Это не то, что совсем невозможно, просто противоречит концепции использования языка и очень сложно технически (особенно если там есть generic’и).

Просто грустно, что когда это все-таки нужно, приходится извращаться. Насколько это технически сложно — судить не берусь, но так обидно, что тип вроде вот он на этапе компиляции, руку протяни только. И пропадает.

Sign up to leave a comment.

Articles