Rust - std::ANY alebo lepší návrh?

Rust - std::ANY alebo lepší návrh?
« kdy: 15. 11. 2021, 18:04:43 »
Majme nasledujuci kod, kde mame strukturu X, Y, Z a majme "Database" ktora obsahuje funkciu "push" - vstup je "Any". Na zaklade typu vstupujuceho do "push" sa rozhodne ktory template sa zavola.
Dany zapis samozrejme funguje, len to nieje to prave orechove, skusenejsi kolegovia, mate nejaky navrh na zlepsenie?

Kód: [Vybrat]
// Various Structs
pub struct X {
    name: String
}

pub struct Y {
    text: String
}

pub struct Z {
    value: u64
}

/* Builder - traits */
trait DatabaseBuilder<T> {
    fn add(&self, data: &T);
}

impl DatabaseBuilder<X> for Database {
    fn push(&self, data: &X){
        /* ... */
        let y = data.name;
    }
}

impl DatabaseBuilder<Y> for Database {
    fn push(&self, data: &Y){
        /* ... */
        let y = data.text;
    }
}

impl DatabaseBuilder<Z> for Database {
    fn push(&self, data: &Z){
        /* ... */
        let y = data.value;
    }
}

/* Database */
pub struct Database {
    /* ... */
}

impl Database {
    pub fn new() -> Databse {
        /* ... */
    }

    pub fn push(&mut self, data: &dyn std::any::Any) {

        match data.downcast_ref::<X>() {
            Some(p) =>  DatabaseBuilder::<X>::push(self, p),
            None => {}
        }

        match data.downcast_ref::<Y>() {
            Some(p) =>  DatabaseBuilder::<Y>::push(self, p),
            None => {}
        }

        match data.downcast_ref::<Z>() {
            Some(p) =>  DatabaseBuilder::<Z>::push(self, p),
            None => {}
        }

    }

}
« Poslední změna: 15. 11. 2021, 18:33:51 od Petr Krčmář »


Ink

  • ****
  • 436
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #1 kdy: 15. 11. 2021, 20:06:28 »
Proč nepoužiješ enum?

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #2 kdy: 15. 11. 2021, 20:52:10 »
mate nejaky navrh na zlepsenie?
Ta funkce push může být klidně generická. Nebo jde mít trait zastřešující X–Z. První řešení je více o (silných) typech, druhé o OO návrhu. Proti použití Any bych obecně nic neměl, ale typ se pak kontroluje za běhu, což nebývá úplně ideální. Navíc v Rustu má Any omezení ohledně lifetimů.

Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #3 kdy: 15. 11. 2021, 23:20:51 »
Mozete spravit kratky snippet, pripadne upravit kod ktory som postol? Nieje mi uplne jasne ako by som to v oboch pripadoch zrealizoval.. v Ruste som novacik tak sa rad naucim novym trikom.
Diky!

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #4 kdy: 16. 11. 2021, 06:42:17 »
Mozete spravit kratky snippet, pripadne upravit kod ktory som postol?
Chtělo by to nejdříve úplný kód (bez /* ... */). U té generické metody by byla signatura
Kód: [Vybrat]
fn push<T:B>(&mut self, data:T) kde B je příslušný bound. Tohle ani není o Rustu, každý jazyk s typovými parametry má takovéto použití unifikace. V obecné rovině doporučuju co nejvíce modelovat typové podobnosti (a odlišnosti) přes traity, dynamický dispatch je na Rustu to zajímavé oproti C.


Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #5 kdy: 16. 11. 2021, 10:16:30 »
Jak píše Idris, asi základní otázka v tvém případě je, jestli push opravdu musí umět zpracovávat Any (tj. za překladu neznáš typ pushovaných dat), nebo jestli ten typ za překladu znáš, a chceš jen, aby push bylo generické a umělo pracovat s různými typy dat.

Pokud typ za překladu znáš (a pokud je to jen trochu možné, snažil bych se to tím směrem tlačit), půjde se nějak odpíchnout od Idrisova nástřelu, když napíšeš víc, napíšeme víc i my.

Pokud ne, asi skutečně nezbude než dělat nějaký dynamic dispatch přes např. Any. I v takovém případě bych se ovšem spíš snažil případná neznámá data co nejdřív převést na nějaký konkrétní typ a udělat push generické s trait boundem a bez Any.

Ink

  • ****
  • 436
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #6 kdy: 16. 11. 2021, 10:28:04 »
Jak píše Idris, asi základní otázka v tvém případě je, jestli push opravdu musí umět zpracovávat Any (tj. za překladu neznáš typ pushovaných dat), nebo jestli ten typ za překladu znáš, a chceš jen, aby push bylo generické a umělo pracovat s různými typy dat.

Pokud typ za překladu znáš (a pokud je to jen trochu možné, snažil bych se to tím směrem tlačit), půjde se nějak odpíchnout od Idrisova nástřelu, když napíšeš víc, napíšeme víc i my.

Pokud ne, asi skutečně nezbude než dělat nějaký dynamic dispatch přes např. Any. I v takovém případě bych se ovšem spíš snažil případná neznámá data co nejdřív převést na nějaký konkrétní typ a udělat push generické s trait boundem a bez Any.

Z kódu v prvním příspěvku bych vyvozoval, že mu nejde o "libovolná data", akorát neví, jak tam procpat nějaký konečný počet různých datových typů. Jakkoli chápu, že při vytváření jednotlivé hodnoty je třeba explicitně uvést variantu a tudíž to je malinko ukecanější, pořád mi enum přijde jako menší zlo, než dynamický dispatch.

Možná by bylo ale fajn, kdyby OP uvedl, co přesně potřebuje udělat, než aby se snažil nějak přiohnout jedno místo v konkrétním návrhu.

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #7 kdy: 16. 11. 2021, 10:32:59 »
Pokud ne, asi skutečně nezbude než dělat nějaký dynamic dispatch přes např. Any. I v takovém případě bych se ovšem spíš snažil případná neznámá data co nejdřív převést na nějaký konkrétní typ a udělat push generické s trait boundem a bez Any.
Přesně. Když už je nutné sáhnout po Any, je dobré jeho užití izolovat a ve zbytku kódu pracovat hezky po rustovsku s typovým systémem (ten je v Rustu ostatně poměrně silný, nově včetně GADT). Traity poskytují transparentní dynamický dispatch, Any je explicitní dynamický dispatch dělaný na koleně (dost často působí jako hack nebo antipattern).

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #8 kdy: 16. 11. 2021, 10:37:32 »
Jakkoli chápu, že při vytváření jednotlivé hodnoty je třeba explicitně uvést variantu a tudíž to je malinko ukecanější, pořád mi enum přijde jako menší zlo, než dynamický dispatch.
Ani jedno není zlo. A enumy jsou taky dynamické (rezoluce za běhu, v Rustu to jsou ostatně součtové typy a interní implementace je stejným dynamickým mechanismem jako traity). Rozdíl je na úrovni syntaxe a potažmo čitelnosti, tam vedou "enumy". Jen to chce pro posouzení vědět přesněji, o co tazateli jde.

Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #9 kdy: 16. 11. 2021, 11:58:34 »
...A enumy jsou taky dynamické (... interní implementace je stejným dynamickým mechanismem jako traity)...

Už jsme asi dosti offtopic, ale toto IMHO není pravda. Pro zajímavost: https://docs.rs/enum_dispatch/0.3.7/enum_dispatch/

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #10 kdy: 16. 11. 2021, 12:38:31 »
Pro zajímavost: https://docs.rs/enum_dispatch/0.3.7/enum_dispatch/
Tohle je skutečně zajímavé, vypadá to jako nějaká sofistikovaná optimalizace.

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #11 kdy: 16. 11. 2021, 13:34:15 »
Pro zajímavost: https://docs.rs/enum_dispatch/0.3.7/enum_dispatch/
Tohle je skutečně zajímavé, vypadá to jako nějaká sofistikovaná optimalizace.
P.S. Teď jsem se dočetl, že překladač Rustu má dostat optimalizaci devirtualizací i pro traity, prý už se na tom pracuje. Pak už zůstane jako nevýhoda jen alokace na haldě, ale to s rozumným alokátorem není problém. Dík za odkaz, donutilo mě to najít si více o interní (zamýšlené) implementaci Rustu.

Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #12 kdy: 17. 11. 2021, 12:47:14 »

Ano, spravne. Jednotlive typy (XYZ) poznam uz v dobe prekladu, nevedel som spravne namodelovat Trait tak, aby som vedel "nacpat" XYZ do funkcie "push", a teda, vyuzil som mne zname std::Any.
Proč nepoužiješ enum?
Este niesom v Ruste spravne "zabehnuty" takze, neviem/netusim ako konkretne by som pouzil Enum ako nahradu za Struct ( tomto konkretnom priklade)

fn push<T:B>(&mut self, data:T)

Ano, k tomuto som sa rovnako dopracoval +- ked ste to postol.

Posielam prehladnejsi, upravenejsi kod, samozrejme v kode pouzivam Result<(), Error>, len pre prehladnost som to odstranil.
Uz sa mi to vcelku pozdava, az na nutnost DatabaseMembers, ale to si myslim, ze casom a skusenostami optimalizujem :)
Diky

Kód: [Vybrat]
use MYSQL::Connection;

pub enum DatabaseMembers {
    X,
    Y,
    Z
}

// Various Structs
pub struct X {
    pub name: String
}

pub struct Y {
    pub text: String
}

pub struct Z {
    pub value: u64
}

/* Builder - traits */
trait DatabaseBuilder {
    fn push(&self, c: &Option<Connection>);
    fn create_table(c: &Option<Connection>);
}

impl DatabaseBuilder for X {
    fn push(&self, c: &Option<Connection>){
        let query = query = "INSERT INTO x (name) VALUES (?1)"
        let query_params = params![&self.name];

        match c {
            Some(e) => e.execute(query, query_params)?,
            None => {},
        };
    }

    fn create_table(c: &Option<Connection>){
        let query =
            "CREATE TABLE IF NOT EXISTS X (
                id      INTEGER PRIMARY KEY,
                name    TEXT NOT NULL,
            );";
        match c {
            Some(e) => e.execute(query, [])?,
            None => {},
        }
    }

}

impl DatabaseBuilder for Y {
    fn push(&self, c: &Option<Connection>){
        let query = query = "INSERT INTO y (text) VALUES (?1)"
        let query_params = params![&self.value];

        match c {
            Some(e) => e.execute(query, query_params)?,
            None => {},
        };
    }

    fn create_table(c: &Option<Connection>){
        let query =
            "CREATE TABLE IF NOT EXISTS X (
                id      INTEGER PRIMARY KEY,
                text    TEXT NOT NULL,
            );";
        match c {
            Some(e) => e.execute(query, [])?,
            None => {},
        }
    }
}

impl DatabaseBuilder for Z {
    fn push(&self, c: &Option<Connection>){
        let query = query = "INSERT INTO z (name) VALUES (?1)"
        let query_params = params![&self.name];

        match c {
            Some(e) => e.execute(query, query_params)?,
            None => {},
        };
    }

    fn create_table(c: &Option<Connection>){
        let query =
            "CREATE TABLE IF NOT EXISTS X (
                id      INTEGER PRIMARY KEY,
                name    INTEGER,
            );";
        match c {
            Some(e) => e.execute(query, [])?,
            None => {},
        }
    }
}

impl DatabaseBuilder for Database {
    fn push(&self, c: &Option<Connection>){};
    fn create_table(c: &Option<Connection>){};
}

/* Database */
pub struct Database {
    c: Option<Connection>,
}

impl Database {
    pub fn new() -> Database {
        Database {
            c: None,
        }
    }

    pub fn open_temporary(&mut self){
        self.c = Some(Connection::open_in_memory());
    }

    pub fn push<T: DatabaseBuilder>(&mut self, data: &T) {
        data.push(&self.conn)
    }

    pub fn create_table(&mut self, m: DatabaseMembers) {
        match m {
            DatabaseMembers::X => X::create_table(&self.conn),
            DatabaseMembers::Y => Y::create_table(&self.conn),
            DatabaseMembers::Z => Z::create_table(&self.conn),
        }
    }
}

fn main() {

    let mut db: Database = Database::new();
    db.open_temporary();


    // X   
    db.create_table(DatabaseMembers::X);
    let x = X {name: String::from("Ahoj")};
    db.push(&x);

    // Y   
    db.create_table(DatabaseMembers::Y);
    let y = Y {text: String::from("Ahoj")};
    db.push(&y);

}


« Poslední změna: 17. 11. 2021, 12:48:49 od Marekuss »

Idris

  • *****
  • 1 721
    • Zobrazit profil
    • E-mail
Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #13 kdy: 17. 11. 2021, 14:14:39 »
fn push<T:B>(&mut self, data:T)
Ano, k tomuto som sa rovnako dopracoval +- ked ste to postol.
Jo, tohle je standard (nejen) v Rustu. Součtové typy (rustí enum) můžou být rychlejší (překladač Rustu traity evidentně nedevirtualizuje).

Re:Rust - std::ANY alebo lepší návrh?
« Odpověď #14 kdy: 17. 11. 2021, 16:05:24 »
fn push<T:B>(&mut self, data:T)
Ano, k tomuto som sa rovnako dopracoval +- ked ste to postol.
Jo, tohle je standard (nejen) v Rustu. Součtové typy (rustí enum) můžou být rychlejší (překladač Rustu traity evidentně nedevirtualizuje).

Když použiješ traitu jako typový parametr funkce (fn f<T>(t: T)...), překladač ji pro konkrétní typ "monomorfizuje", tj. přeloží specializovanou verzi přímo tomu typu na míru. To se výkonově těžko dá překonat.

Když použiješ traitu jako trait object (Box<dyn T>, nebo &dyn T, které naházíš třeba do Vec, abys mohl mít "heterogenní" kolekci), dostaneš fat pointer - vtable+data. Volání se děje dispatchem přes tu vtable. Devirtualizovat to asi vždy půjde jen omezeně, v principu může být těch implementací (tj. různých vtablů) spousta.

Když použiješ enum, je to obyčejný datový typ, tj. žádný fat pointer, na konkrétní variantě matchuješ, a když je těch variant málo, je vcelku zjevný, že to matchování se dá implementovat rychleji (jedním dvěma ify, což pak může spekulativní provádění instrukcí vykonat prakticky hned), než dereference přes vtable (i když samozřejmě když pak v kolekci bude třeba jeden typ trait objectu výrazně převažovat, tak to díky cache a branch predikci nejspíš bude taky docela odsejpat, ale i tak je to alespoň dvakrát tolik dat než jednoduchý enum).

V praxi se samozřejmě člověk mezi enumem a trait objectem rozhoduje nejvíc podle toho, jestli jde o uzavřený nebo otevřený typ.