Původně jsem si myslel, že když už udělaji nový "bezpečnější" jazyk, bude snaha omezit programátora syntakticky tak, aby nemohl příliš "prasit" a zapsat vše pouze jedním - a tím správným způsobem, ale tak to jak si pročítám seriál o Rustu (podle mě bohužel) není. Objevuje se tady "takto je to idimaticky správně", lze to "naprasit" i takto... Velmi to připomíná současné C++, ano pokud to budete "psát idiomaticky správně" eliminujete hromadu možných bezpečnostních problémů (http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines), ale můžete to psát i jinak a ještě hůře... (ano C++ je v tom dále z toho důvodu že je straší). S posledními aktualizacemi C++11..17 bych řekl, že se C++ dostává někam na úroveň Rustu i z hlediska bezpečnosti zápisu (jen to stále lze "naprasit" i dalšími a mnohem horšími způsoby).
Pokud chceš jazyk, který tě omezuje v tom, co se jak má napsat a použít, podívej se spíš na golang. Rust má jinou filosofii a není tak striktní. V Rustu neexistuje "ta jediná správná cesta", jak by se něco mělo dělat. Je to dáno taky tím, že se snaží být multiparadigmatický, je imperativní, ale současně můžeš používat i funkcionální prvky. C++ se nikdy nedostane co se týká bezpečnosti na úroveň Rustu, protože není memory safe.
Bohužel to že je Rust memory-safe je lež! :-( Bylo tomu tak, ale z důvodu výkonu odstranili morestack (s tím že správné/lepší řešení je -fstck-check). Jenže v LLVM je -fstack-check no-op, není implementovaný a kdoví kdy bude. Jediná platforma kde je implementovaný jsou Windows.
Tzn. Rust je memory-safe pouze na Windows, na všech ostatních platformách _není_ memory-safe.
Viz diskuze u pull requestu: Remove morestack support #27338
Nesegfaultuje, pada na stack overflow (a jeste kompilator docela slusne drzkuje):
---- sf.rs:
fn r(a: &[i8]){
let a: [i8; 8193] = [0; 8193];
r(&a);
}
fn main()
{
let a: [i8; 8193] = [0; 8193];
r(&a);
}
---- kompilace:
rustc sf.rs
warning: unused variable: `a`, #[warn(unused_variables)] on by default
--> sf.rs:1:6
|
1 | fn r(a: &[i8]){
| ^
warning: function cannot return without recurring, #[warn(unconditional_recursion)] on by default
--> sf.rs:1:1
|
1 | fn r(a: &[i8]){
| ^
|
note: recursive call site
--> sf.rs:3:5
|
3 | r(&a);
| ^^^^^
= help: a `loop` may express intention better if this is on purpose
---- spusteni:
./sf
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Aborted (core dumped)
Zkuste to spustis vícekrát. Bez morestack (a implementovaných stack probes, které by ho měly nahradit) je to více méně náhodné, někdy to Rust chytí a někdy ne. Prostě dobrovolně obětovali memory-safety na všech platformách kromě Windows, jen aby získali pár procent výkonu navíc :-(
Vcera jsem to zkousel, fakt to obcas hlasi segfault. Otazka ale je, co se ve skutecnosti deje. Mel jsem za to, ze kdyz se tohle stane, Rust (jiny jazyk kompilovany do strojaku) se snazi zapsat, kam uz nema (mimo vyhrazeny stack) a system proces sejme. V lepsim pripade to runtime stihne detekovat a vypise korektni stack overflow (ano, bylo by super, kdyby to spolehlive fungovalo), ale k zadne nepredvidatelne akci (krome padu) by to vest nemelo. Pletu se?
Priznam se, ze Rust neznam. V adresnim prostoru procesu na Linuxu, jsou zasobniky polozene za sebou. Tzn. tam kde konci prvni stack, zacina stack dalsiho threadu. Mezi nimi je jedna zamcena stranka, na kterou nesmite sahnout, jinak dojde k segfaultu. Melo by to pomoct vyresit situace kdy stack overflow, prepise stack druheho vlakna. Takove chyby by se hodne blbe ladily.
Pokud ale alokujete pamet na zasovnibku po prilis velkych kusech, muze se vam stat, ze tu zamcenou stranku preskocite a druhemu vlaknu stack stejne prepisete.
Ale samozřejmě nikdo nikomu nebrání používat namísto Rustu Haskell nebo Ocaml.
Bohužel v obou jazycích existují a poměrně často se v běžných knihovnách a programech i používají funkce, kterými lze o memory safety přijít. (Nemluvím o FFI.)
Dokud mi někdo neukáže praktický a smysluplný příklad zneužitelnosti, považuju to za hnidopišství.
Já tomu říkám lhaní. Píší guaranteed memory safety, a to nejenže na to nemají žádný formální důkaz, ale dokonce vědí, že opak je pravdou.
Celkově je těch omezení dost. Nějaké idomy tam pochopitelně jsou (třeba psát match místo if), ale Rust omezuje hodně.
No a navíc - spousta legálních věcí při kompilaci zahlásí warning, že "takhle raději ne". A warningy jsou on by default.
A jako bonus je tu formatter, který udržuje i jednotné formátování mezi projekty.
Příklad pár warningů z jednoho mého malého narychlo napsaného prográmku:
warning: unnecessary parentheses around assigned value, #[warn(unused_parens)] on by default
warning: unnecessary parentheses around `if` condition, #[warn(unused_parens)] on by default
warning: unused import, #[warn(unused_imports)] on by default
warning: unused variable: `platform`, #[warn(unused_variables)] on by default
warning: function `kupfuCheckSum` should have a snake case name such as `kupfu_check_sum`, #[warn(non_snake_case)] on by default
(Nevim, jestli nahodou nenarazim na totez, co myslel vyse Jvr z VSE.) Vsechno se mi to libi, ale tohle me desi:
pokud z funkce pass_by_reference odstraníte hvězdičky (operátor dereference), proběhne překlad v pořádku. Totéž platí i při volání funkce, kdy je možné zapsat znak & kolikrát chcete. Je to zvláštní a užitečný důsledek silné typové kontroly a typové inference.
Je pro takovehle chovani nejaky rozumny duvod? Mam pocit, ze kdekoli byla v normach benevolence, vetsinou to dost hrozne dopadlo. Viz rana leta HTML, kdy se kazda jedenapulta prirucka chvastala, jak je to bozi, ze lze napsat <a><b></b> a AI si to sama spravne prebere. Jak tenhle trend dopadl, vsichni vime. Jelikoz ale predpokladam, ze za vznikem Rustu je mozek a ne tarot, mozna to nejaky vyznam ma, jen mi proste nedochazi.
Ono je těch "magických" konverzí ve skutečnosti ještě víc, docela hutně je to popsáno tady:
https://github.com/rust-lang/rfcs/blob/master/text/0401-coercions.md
U těch referencí je to celkem šikovná věc, protože se v tomto místě překladač IMHO nemůže "splést" - při volání přesně ví, jaký typ přesně očekává, navíc ví, že programátor explicitně vytvořil referenci.
Hm. Překladač sice ví, ale otázka je, zda ví (v daném kontextu) i člověk, který ten kód čte...
IMHO čím víc implicitního chování, tím hůř se kód chápe a to má v důsledku vliv na cenu a kvalitu. Přecejen pořád platí, že jednou napsaný kód bude stokrát přečtený... Naprosto souhlasím s robotronem, že jazyk by měl být poměrně striktní, co se zápisu týče.
> Hm. Překladač sice ví, ale otázka je, zda ví (v daném kontextu) i člověk, který ten kód čte...
Otazka je, zda to ten clovek potrebuje vedet ;-) Ve vetsine pripadu kdyz mam:
obj1->obj2.obj3->obj4
je pro me dulezite ze pristupuju k atributum objektu, a to ze nekde musi byt "." a jinde "->" je pro me jenom informacni sum... ;-)
Modifikator out je pozustatkem doby, kdy mainstreamove jazyky neznaly tuple
out
může být v mnoha situacích přehlednější, pokud jazyk neumí tuple s pojmenovanými prvky (jako třeba Swift) neboli anonymní záznamy.
Navíc out
může být efektivnější, pokud se tuple alokuje na heapu a kompilátor tuto alokaci neeleminuje.
> out může být v mnoha situacích přehlednější, pokud jazyk neumí tuple s pojmenovanými prvky (jako třeba Swift) neboli anonymní záznamy.
To jako ze chce vracet zpatky treba 5 hodnot? Tam uz je to podle me na explicitni typ.
> Navíc out může být efektivnější, pokud se tuple alokuje na heapu a kompilátor tuto alokaci neeleminuje.
Pokud ale dany jazyk ma neco spatne, tak by bylo namiste to spis opravit, namisto takoveho pseudoreseni.
kde by byl problem v nepojmenovani dvou hodnot ve dvojici
Často je to problém, když funkce vrací 2 hodnoty stejného typu (snadno dojde k jejich záměně). Nebo když takových dvojic používáte více na jednom místě (např. v jedné funkci).
Nevýhodou pozičního pattern matchingu (tj. když záleží na pořadí) je, že kód příliš nezpřehlední, když jsou tuply vnořené, další nevýhodou je, že pro přidání fieldu do tuplu musíte upravit všechna místa, kde tuple srovnáváte se vzorem (zatímco, když si fieldy pojmenuje, tak není třeba nic upravovat).
Pokud je jazyk staticky typovaný, typ byste musel explicitně vytvářet tak jako tak, jen by možná byl inline, takže by nepotřeboval jméno. Takže jméno je to jediné, co potřebujete navíc. To není až tolik?
Ale jak pracovat s takovýmto inline typem? Pokud bych z nějakého důvodu chtěl explicitně otypovat výstup z té funkce (pokud ho budu chtít předávat jinam, bude se to docela hodit…), najednou ten pojmenovaný typ bude praktičtější.
Takže jméno je to jediné, co potřebujete navíc. To není až tolik?
Potřebuji celou tu deklaraci, ne? Tj. jméno + seznam fieldů + jejich typy. Když třeba píši SQL dotazy, přijde mi už docela dost práce pro každý dotaz vytvářet pojmenovaný typ. A vracet z SQL dotazů tuply je z mé zkušenosti v F# docela nepřehledné (ale bohužel LINQ v F# nic lepšího neumožňuje).
Pokud bych z nějakého důvodu chtěl explicitně otypovat
S tím samozřejmě souhlasím.
> seznam fieldů + jejich typy
Což bych ale musel uvádět tak jako tak, přinejmenším ta jména jo. Typy by si kompilátor mohl teoreticky odvodit, ale otázka je, nakolik by to bylo best practice. Třeba u Scaly se považuje za best practice mít explicitní typové anotace u návratových hodnot v public API., což mi přijde rozumné.
Ano, daly by se najít případy, kdy by jazyk mohl poskytnout pohodlnější řešení návratových hodnot. Třeb vracím pět různých hodnot a nejsem v public API a typ pojmenovat nepotřebuju. Ale jak je to často? Pokud moc ne, nevedlo by to spíše k málo známé syntaxi, která čtenáři zkomplikuje čtení kódu?
Ale jak je to často?
Mně to právě přijde docela časté - SQL, Spark, n-tice, která je použita pouze jako návratová hodnota jedné funkce, automaticky generovaná schémata z různých formátů (kolikrát z formátů typu JSON není patrné, jak záznam pojmenovat nebo to dokonce ani nemá rozumný název).
Na mnoha místech, kde se dnes používají dvojice, trojice nebo nedej bože n-tice pro n>3, by šlo použít anonymní záznam / tuple s pojmenovanými složkami a zlepšilo by to čitelnost, zabránilo některým chybám a umožnilo snáze přidat další složku.
Podobná situace je mj. i s algebraickými datovými typy - mnoha jazykům chybí anonymní a místo toho používají Either (někdy i vnořený nebo Either3), kde z typu funkce nic nepoznáte.
Což bych ale musel uvádět tak jako tak, přinejmenším ta jména jo.
Jména můžete vzít z proměnných, která do záznamu ukládáte nebo ze sloupců v SQL dotazu, typ záznamu se pak inferuje.
Mně se moc nelíbí, že pořád hledáš, jak odůvodnit špatné (resp. nedokonalé) řešení problému způsobeného jiným špatným rozhodnutím nebo absencí nějaké další featury. Pojďme se pobavit o tom, jak a zda by se dal vylepšit Rust/Scala/F#/Haskell nebo já nevím co, aby se to nemuselo záplatovat nějakým zhůvěřilostmi jako je "výstupní parametr".
Pobavila mě diskuse na fóru Rootu, kde se řeší, jaký chaos by nastal při zrušení primitivních typů v Javě, protože by se někde ve volání funkce mohl objevit null. Přičemž samozřejmě jde o to, že tu chlívárnu jménem null vůbec do Javy tvůrci zavlekli, protože neznali Option/Maybe a pattern matching nebo se domnívali, že je to moc složitý koncept pro programátory v Javě.
zda by se dal vylepšit Rust/Scala/F#/Haskell
Však říkám - přidat anonymní záznamy a umožnit, aby se nemusely alokovat na heapu.
Koneckonců o anonymní záznamy se ve Scale a Haskellu pokouší různé knihovny. Problém je, že tyto knihovny se většinou nepoužívají úplně pohodlně - proto se třeba uvažuje o přidání záznamů do Dotty - viz https://github.com/lampepfl/dotty/issues/964 A kdysi se to zvažovali i pro GHC Haskell http://research.microsoft.com/en-us/um/people/simonpj/Haskell/records.html (Hugs měl Trex). Pro F# rovněž existuje návrh https://github.com/fsharp/fslang-suggestions/issues/207 , ale nejspíš nebude v brzké době implementován, protože se neví, jak to implementovat nad CLR, aby to bylo efektivní.
Přičemž samozřejmě jde o to, že tu chlívárnu jménem null vůbec do Javy tvůrci zavlekli, protože neznali Option/Maybe a pattern matching nebo se domnívali, že je to moc složitý koncept pro programátory v Javě.
Se ti to dobre keca z pohledu roku 2016. Ale uvedom si, kdy zacinal vyvoj Javy a kdy byly zverejnene prvni verze, tj. prvni polovina devadesatych let, kdy bezne pocitace mely procesory i286-386 a 1 az 4 MB pameti RAM. Tam uz kazda alokace navic pro Optional/Maybe je znat. Ke vsemu jeste Java nebyla urcena pro bezne PC ale pro embedded zarizeni, takze tam to bylo s moznostmi HW jeste horsi. Z pohledu navrha jazyka jsou primitivni typy vetsi zlo nez null. Pricemz, ze by null byl nejake velke zlo, se zacatkem devadesatych let moc neresilo, lidi meli s pocitaci jine starosti.
tu chlívárnu jménem null vůbec do Javy tvůrci zavlekli, protože neznali Option/Maybe a pattern matching nebo se domnívali, že je to moc složitý koncept pro programátory v Javě
Dnes jde null
docela pěkně ošetřit typovým systémem (viz třeba Kotlin). Takový kód v Kotlinu je elegantnější než obdobný kód ve Scale s Option
.
Je otázkou, zda Option
plně řeší problém null
, v situaci, kdy máte proměnnou (nebo field), do níž přiřazujete a která může v určité části programu (např. při inicializaci) být None
a v určité části programu ne. Takto přesný typ většina jazyků nedokáte vyjádřit, tudíž programátor musí použít aproximaci, která jednoduše řekne, že proměnná je typu Option
- tj. v části kódu, kde proměnná nemůže být None
, je na tom podobně jako s null
.
> může v určité části programu (např. při inicializaci) být None a v určité části programu ne
Inicializace – to řeší (v tak 99 % případů) třeba modifier final v Javě.
> tudíž programátor musí použít aproximaci
I to se může stát. Rozdíly mezi null a Option:
* Option lze vnořovat, nullability ne.
* Null bývá přesně z tohoto důvodu efektivnéji implementovaný. I když nmusel by, ve Scale jsem se pokoušel implementovat vlastní Option, která by se překládala interně na null, kde by to šlo. (Nešlo by to třeba u vnořených Option nebo u generičtějšího kódu.) Bohužel jsem tehdy narazil na nějaký bug.
* Neumí-li jazyk dobře pracovat s nullability, hodnota null může překvapit. Programátor bude tiše předpokládat, že se null objevit nemůže, zkompiluje se to a za běhu (třeba až na produkci…) se tam null objeví. Aby se to stalo u Option, musel by programátor například vědomě zavolat get(). V tak 99 % případů se bez get() lze obejít.
* S Option lze dělat map/filter/exists/… Pravda, u null to v principu jde také.
Inicializace – to řeší (v tak 99 % případů) třeba modifier final v Javě.
Zrovna v Javě je problém s tím, že metodu nejde volat s pojmenovanými argumenty - tj. volání metod s mnoha argumenty jsou nepřehledná, tj. u objektů, kde je k inicializaci třeba mnoho argumentů, je použití konstruktoru nepřehledné. Pokud se tedy inicializaci rozhodnete provádět mimo konstruktor, final
nepomůže.
Neumí-li jazyk dobře pracovat s nullability, hodnota null může překvapit.
Souhlasím, je to spíš problém konkrétní implementace než problém null
.
implementovat vlastní Option, která by se překládala interně na null, kde by to šlo.
BTW F# to takhle má implementované (na CLR jde volat metody, i když je this == null
).
"Zrovna v Javě je problém s tím, že metodu nejde volat s pojmenovanými argumenty - tj. volání metod s mnoha argumenty jsou nepřehledná, tj. u objektů, kde je k inicializaci třeba mnoho argumentů, je použití konstruktoru nepřehledné. "
Tohle se řeší nějakým návrhovým vzorem ne? To, že můžu pojmenovat argumenty je pěkné, ale když mám konstruktor s 20 argumenty, ani to mi nepomůže. Rust právě hojně používá Builder Pattern.
Rust právě hojně používá Builder Pattern.
Jenže v Javě vás Builder nezachrání před null
(kompilátor Javy nijak nezabrání dereferenci null
v metodách builderu - a final
nejde v builderu použít, pokud bude builder měnit hodnotu proměnné).
ale když mám konstruktor s 20 argumenty, ani to mi nepomůže.
Proč?
Tak ono mnohdy stačí umět na null správně reagovat a ne hned házet NPE. Prostě chápat null jako "tady není žádná hodnota" a ne "tady není nic a strašně se s tím musíš poprat". Jo když se to chápe rigidně, tak i klasický výraz "Hello " + name musíš obalit checkem na not null. Takové Clojure na to kašle, prostě (str "Hello " name) nikdy nehodí NPE (tady to tak nevypadá, ale ve složitějším kódu, kde se dělá pipa pomocí makra -> to v podstatě ušetří hromadu zbytečných if-ů)
> SQL
Tam typicky chci ten typ i pojmenovat, protože se bude předávat dál.
> kolikrát z formátů typu JSON není patrné, jak záznam pojmenovat nebo to dokonce ani nemá rozumný název
Zrovna tady jsem kdysi zkoušel dělat analyzátor struktury JSONu (dostal příklad a vypadla z něj definice tříd) a nažvy tříd vymýšlel celkem dobře heuristicky. A pokud to zvládne stroj, člověk by s tím uz vůbec neměl nít problém.
> mnoha jazykům chybí anonymní a místo toho používají Either
Either mi přijde vpohodě, na Either3 a výš si radši napíšu abstract sealed class (ve Scale). Kdyby šlo takový typ definovat anonymně, stejně bych dřív nebo později většinou potřeboval typedef.
> Jména můžete vzít z proměnných
OK. U neveřejného API si to umím představit. U veřejného API bych stejně chtěl explicitní typovou anotaci (aby inferovaný typ nebyl příliš specifický, což by mohlo vadit při budoucích úpravách) a tam se tomu pak nevyhnu.
Tam typicky chci ten typ i pojmenovat, protože se bude předávat dál.
Mně přijde docela těžké vymyslet rozumné názvy pro takové množství typů - např. při dotazech do tabulky s uživateli mohu potřebovat názvy typů pro různé podmnožiny { id: int, realname: string, email: string, nickname: string, ... }
- může to být klidně i 10-20 typů pro jednu tabulku. Ve skutečné aplikaci pak budou třeba i typy, kdy se joinovalo více různých tabulek.
na Either3 a výš si radši napíšu abstract sealed class (ve Scale)
Nevýhodou tohoto přístupu je, že pokud máte funkce, kde jedna vrací např A/B/C, druhá B/C, třetí A/C, čtvrtá A/D a chcete napsat funkci, která umí zpracovat A/B/D, tak se to celkem těžko vyjadřuje (pokud to vůbec jde, musíte ve Scale zkonstruovat podivnou hierarchii). Tohle řeší třeba shapeless pomocí typu Coproduct, ale ten má zase jinou řadu nevýhod. Dotty již tohle zvládne díky union typům.
Další nevýhodou je, že musíte vymýšlet názvy pro ty typy - někdy to je dost těžké a skončí to názvem typu NázevFunkceResult
.
Vhodne udelany querybuilder muze generovat i typy, nebo naopak z typu generovat seznam sloupcu do selectu. Beztak chce mit clovek osetreno cteni z databaze, ze sloupec je spravneho typu, a pokud ne tak !unreachable() nebo !panic(). Ostatne cteni z databaze nebo zapis do databaze je specialnim pripadem serializace/deserializace. Pokud jazyk umoznuje obecnou serializaci/deserializaci napriklad pomoci introspekce, tak by v nem takovy querybuilder mel jit napsat. (Tohle jsem nedavno pachal v golangu.)
Tak napriklad Decko tuple zna a podporuje, a ano vetsinou jej vyuziva kde to ma smysl, ale i tak tam out ma sve misto, je to neco jako ref se specialni semantikou, krom toho ze tim clovek oznacuje referenci ktera ma znazornovat vystup a obcas se to muze hodit, tak zaroven rika ze je defaultni hodnota inicializovana dle typu.
Tak jeden z pripadu kdy se out hodi je pouziti UFCS.
auto funOut(out size_t length) {
length = "ahoj".length;
return "ahoj";
}
size_t length;
auto res = funOut(length).someFun.anotherFun.andNextAnotherFun(length);
pokud bych mel tuple tak bych musel udelat toto
auto tmp = funOut();
auto res = tmp[0].someFun.anotherFun.andNextAnotherFun(tmp[1]);
Dalsi pripad uziti je kdyz potrebuji predat OutputRange, to se da predat sice referenci ale kdyz tam clovek zada to out tak ma jistotu ze v tom OutputRange nic nezbylo a hlavne to slovou out ma dokumentacni charakter ze clovek vi ze to je pouzito pro vystup, pokud by tam bylo ref, tak to muze a nemusi byt pouzito pro vystup.
Out parametry jsou imperativní styl. První příklad mi přijde špatně čitelný, není moc jasné, kdy se čte a kdy píše atd.
S tuples a pattern matchingem ten druhý příklad může vypadat nwjak takto:
auto (s, length) = funOut();
auto res = s.someFun.anotherFun.andNextAnotherFun(length);
Což mi přijde mnohem srozumitelnější.
Pozor na to, ze Rust ma taky 'ref', ale pouzivat se ma jen nekde:
https://internals.rust-lang.org/t/use-of-ref-must-in-let-statements/1466
Je to z důvodu zjednodušení práce, Rust provádí automatickou dereferenci při volání funkce. Může to udělat, protože compiler má dostatek informací k tomu, aby takovou konverzi provedl automaticky: https://doc.rust-lang.org/book/deref-coercions.html
Podobná situace je i v C++ s operátory -> a . (tečka). I C++ compiler má dostatek informací k tomu, aby mohl automaticky konvertovat -> na . a opačně, ale neděláto to spíše z historických důvodů. Nicméně i v C++ by to šlo a mohl by existovat jen jeden operátor.
Uspora 1 znaku je asi irelevantni, patrne je tedy jadrem: "nemusím přemýšlet, kolikrát ji mám napsat." Trochu se bojim, aby tato dusevni uspora nebyla vyvazena tim, ze to pak zlenivsi clovek nekde jinde pokazi (kde uz dvoji vyklad zapisu mozny bude). Treba se to v Rustu ale stat nemuze.
Kazdopadne dekuji za odpoved.
Obecně v C++ nelze automaticky konvertovat mezi . a ->. Např:
auto p = std::make_unique<std::unique_ptr<int>>(nullptr); std::cout << p.get() << std::endl; std::cout << p->get() << std::endl;
vypíše dvě různé hodnoty, protože p.get() volá metodu objektu p, ale p->get() volá metodu objektu *p, protože p->get() je totéž, jako p.get()->get() nebo (*p.get()).get().
Syntakticky shodné nebo vzájemně zaměnitelné a kompilátorem automagicky rozpoznané zápisy hodnot, referencí a ukazatelů mi přijdou spíš matoucí než užitečné.
Tohle je taky historické dědictví, unique_ptr "chytře" využil toho, že existují dva různé operátory -> a tečka a chování -> přetížil, takže vlastně dělá něco úplně jiného, než normálně. Jestli je to dobře nebo špatně, to už nechám na posouzení každého soudruha.
Historie operátoru -> ale sahá až do roku 1975, kdy fungoval trochu jinak, než jako funguje v současném C. V tehdejším C bylo naprosto legální napsat tohle:
struct S { int a; /* 2 bytes, offset 0 */ int b; /* 2 bytes, offset 2 */ }; int i = 5; i->b = 42; /* Write 42 into `int` at address 7 */ 100->a = 0; /* Write 0 into `int` at address 100 */
Tehdejší C vyžadovalo, aby položky všech struktur měly unikátní jména, takže položka struktury byl jen takový alias k offsetu adresy. Proto bylo možné napsat:
int i = 5; i->b = 42; /* Write 42 into `int` at address 7 */ 100->a = 0; /* Write 0 into `int` at address 100 */
Nebylo ale možné místo toho použít:
int i = 5; (*i).b = 42; /* Write 42 into `int` at address 7 */ (*100).a = 0; /* Write 0 into `int` at address 100 */
protože nelze dereferencovat integer. Bylo tedy nutné mít rozdílné operátory -> a (*). V moderním C(++) už nemá operátor -> smysl, jeho chování je ekvivalentní s (*). a compiler by mohl mít jen jeden operátor . a provést automatickou dereferenci pointeru, nicméně operátor -> v C zůstal.
Podrobně je to popsáno zde: http://stackoverflow.com/questions/13366083/why-does-the-arrow-operator-in-c-exist
uff tedy popravde to byl peknej hnus, a to jsem si myslel, ze a[100] = 100[b] je to nejhorsi, na co lze v cecku narazit :)
Jinak kdyz jsme u toho, Placal to ma taky, akorat to pouziva jeste hur zapsany ^., to uz ta nase sipka -> je mnohem citelnejsi a vlastne graficky ukazuje, o co jde :)
Právě že v moderním C++, kde se dost používají chytré ukazatele, má -> dobrý smysl a není redundantní, minimálně do doby, než bude možné přetížit operátor tečka. To už nemá nic společného s chováním -> v prehistorické verzi jazyka C.
Abychom se vrátili od C++ k tématu tohoto seriálu, unique_ptr je ukázka toho, jak je v C++ možné prostředky jazyka definovat nové typy (zde chytré ukazatele), které syntakticky vypadají a sémanticky mají některé vlastnosti stejné jako vestavěné typy jazyka. Zároveň ale k vestavěným typům přidávají něco navíc. Mě by zajímalo, zda a jak efektivně je něco takového možné udělat v Rustu.
Dva operátory tečka a -> v C++ se aktuálně (zne)užívají u smart pointerů dost, nicméně ono to má svoje úskalí, překladač nemůže kontrolovat chyby programátora, pokud v C++ napíšu:
if (unique_ptr_object.get())
if (unique_ptr_object->get())
tak obě konstrukce jsou naprosto v pořádku a přeloží se, každá ovšem dělá něco úplně jiného. Je velmi snadné splést se a překladač nemá žádnou možnost, jak chybu odhalit. Znovu opakuji, je to spíš z historických důvodů, C(++) dva operátory tečka a -> nepotřebuje, stačila by tečka.
V Rustu podobná chyba nemůže nastat, jazyk tomu přímo zabrání, protože obě konstrukce:
object.get();
(&object).get();
Dělají úplně to stejné, v Rustu se nejde splést.
Co se týká nových typů, tak Rust má traits a operator overloading:
https://doc.rust-lang.org/book/traits.html
https://doc.rust-lang.org/book/operators-and-overloading.html
To, že dvě různé konstrukce p.f() a p->f() dělají každá něco jiného, není až tak překvapivé. Ano, programátor se může splést. Ale podobně se může splést a napsat a + b místo a - b. Celá tohle dohadování se, kde je a není snadné se splést, je založeno na osobních preferencích a má sice velký potenciál rozpoutat flamewar, ale těžko povede k nějakému rozumnému závěru.
Ve druhé části jsem se ptal na něco trochu jiného. Např. v C++ můžu vyrobit typ checked_int, který bude vypadat a chovat se stejně, jako nativní int, ale u aritmetických operací bude vyhazovat výjimku při přetečení nebo dělení nulou. Jde něco podobného udělat v Rustu tak, aby přidaný typ zapadl do jazyka tak, že se při jeho použití nebudu muset uchylovat k nějakým speciálním syntaktickým konstrukcím a nevznikne nějaká neefektivita, např. skryté alokace paměti?
Ano přesně toto mi přijde tak trochu zvláštní. Jakmile totiž začnete něco řešit "v tom případě má překladač dost informací" (není tam potřeba derefernci psát)... apod. začíná z toho být trochu zmatek. Já nejsem překladač abych poznal kdy on už má dost informací a kdy ne a navíc ten člověk, co se ten kód po mě (možná) někdy v budoucnu bude snažit přeluštit, taky není překladač a právě tam vzniká nevíce chyb (úprava jiným programátorem).
A teď babo raď: "iter1.filter(|x| even(*x));" je tam skutečně potřeba ta hvězdička? Pokud ne je idiomaticky správné ji tam psát nebo nepsat?
To předávání parametrů je trochu nešťastně napsáno a moc nevysvětluje, co se vlastně děje. Rust si vynucuje svůj Ownership/Borrowing model. Takže při
fn funkce(arg1: typ) {...}
si prostředí funkce bere vlastníctví arg1 a v prostředí které volalo funkci k arg1 už nelze přistupovat - zásadní rozdíl od předání hodnotou v C. Co ovšem může být matoucí a opravdu by to mělo být zdůrazněno, jsou traity (obdoba třeba interfaců v Javě) Clone a Copy. Clone říká, že se z typu dá udělat hluboká kopie a poskytuje k tomu funkci .clone()
, Copy můžou implementovat jen typy implementující zárověň Clone a říká, že kopie se udělá přesným zkopírováním hodnoty a zárověň se pří předání preferuje kopie nad předání vlastnictví (důležité! protože proto se může zdát že primitivní typy nějak nezapadají do Ownership/Borrowing modelu, ve skutečnosti jen implementují Copy).
Rust kontroluje správnost implementací tím, že struct může implementovat Clone pokud všechny jeho složky jsou taky Clone a stejně u tak Copy. Takovéhle stupňující implementace jsou použity i u Send a Sync - traity pro multithreading říkající že typ se dá zdílet mezi vlákny a že se dá zdílet reference.
Pro usnadnění implementace se dají Clone a Copy implementovat automaticky pomocí #[derive(Clone, Copy)]
Dále reference &
a &mut
implementují trait Deref který při přístupu k ní automaticky dereferencuje. Dá se to použít na různé wrappery, třeba vektor Vec můžeme díky tomu indexovat, ale k tomu navíc drží kapacitu a zaplnění.
Díky moc za doplnění. No popravdě jsem nechtěl o owneshipu-borrowingu mluvit už v této části, už tak to asi lidi dostatečně mate i bez něho :-) Jinak asi tedy jen doplním, že vlastnictví se neřeší jen pro funkce, ale například i pro "jednoduché" přiřazení (akorát to znovu pro primitivní typy není patrné, protože implementují Copy trait, ale už pro struct nebo Vec to vyhodí chybu, u vektorů kvůli kontrole, kolik existuje ukazatelů na objekt na heapu).