Dart Typesystem

Ľudia zvyknutí na Java, C#, či C++ ohŕňajú nosom nad tým, že Dart je dynamicky typovaný. Ľudia odchovaní na Pythone, Javascripte či Ruby ohŕňajú nosom nad tým, že Dart to s podporou dynamických features príliš nepreháňa. Ľudia obľubujúci Dart nosom neohŕňajú a Dart-ovský typesystem pokladajú za najlepší vynález hneď po krájanom chlebe. O čom táto kontroverzia vlastne je, a ako to celé funguje?
Seriál: Úvod do Dartu (9 dílů)
- Dart – Čo? Prečo? 2. 8. 2013
- Dart – Úvod do jazyka 23. 8. 2013
- Dart – Ponorme sa hlbšie 6. 9. 2013
- Dart – v DOMe 19. 9. 2013
- Dart – Futures 4. 10. 2013
- Dart – Streams 17. 10. 2013
- Dart – Používame JavaScript 1. 11. 2013
- Dart Typesystem 19. 11. 2013
- Dart – Neznesiteľná ľahkosť asynchrónneho bytia 2. 12. 2013
Nálepky:
Dart o sebe na oficiálnej stránke tvrdí, že je dynamicky typovaný. Toto tvrdenie hneď aj dokladá ukážkou, ktorej parafrázu si ukážeme:
var i = 10;
i = new Object();
i = "dart IS dynamically typed language";
print(i.length); // vypise 34
Ukážka sa skompiluje, spustí a korektne vypíše výsledok, úplne ignorujúc problém (problém?), že premenná i
menila typ (číslo, objekt, string), ako sa nám zachcelo. Ak sa „spýtame“ editora, čo si myslí o type premennej i
, (podržíme nad ňou myš), dozvieme sa, že je typu dynamic (čítaj: hocičo). Keby sme do kódu pridali riadok print(i.runtimeType);
dozvedáme sa, že i
je String.
Trochu poriadku do toho dynamična
Zameňme v ukážke deklaráciu var i = 10;
za num i = 10;
. Pre úplnosť: num
je spoločným predkom tried int
a double
. Nastanú dve veci – editor nám ukáže warning a pokiaľ ho budeme ignorovať (jasné, že budeme), program havaruje s hláškou:
"type 'Object' is not a subtype of type 'num' of 'i'."
Na prvý pohľad to môže pôsobiť tak, že pridaním informácie o type premennej i
sme zapli akýsi iný – typovo orientovaný – Dart. To však nie je pravda, dokonca, je to úplne MYLNÁ predstava. Dart totiž zo svojej podstaty ignoruje informácie o type. Tie slúžia (doslova) len na okrasu (teda, pomáhajú ľuďom a tiež DartEditoru rozumieť kódu) a na samotný beh kódu nemajú vplyv.
Počujem nesúhlasné hvízdanie? Ak Dart ignoruje typy, tak prečo náš program havaruje s hore uvedenou výnimkou? DartEditor defaultne púšťa kód v tzv. checked móde. V praxi si to môžme predstaviť tak, že za každým priradením sa prevedie kontrola, či premenná má správny typ, ak nie, vyhodí sa výnimka. Schválne, skúste checked mód vypnúť (v run/manage launches) a všetko pobeží hladko ako predtým. Checked mód je zamýšľaný ako pomôcka pre vývoj, že to je fakt dobrá pomôcka mi asi uverí každý, kto skúsil v JavaScripte napísať viac ako 4 riadky kódu.
Pred filozofickým zamyslením na záver ešte dve zastaveniahodné záležitosti.
Typ pre funkciu
Majme funkciu, ktorá ako svoj parameter akceptuje inú funkciu (toto sa v Darte a tiež kope iných kultúrnych jazykov dá spraviť). Vezmime napríklad takýto kus kódu:
num feedAll(List<Animal> animals, dynamic feedOne){
num consumed = 0;
for(Animal animal in animals){
consumed += feedOne(animal);
}
print("${consumed} food was consumed");
}
//neskor volame feedAll
feedAll([alfie, balthazar, carly], (animal){
num foodConsumed = animal.feed();
print("feeding ${animal.name}. NOM NOM NOM");
return foodConsumed;
});
Funkcia feedAll
akceptuje ako argument feedOne
, ktorého typ sme zatiaľ kvôli nedostatku kreativity špecifikovali ako dynamic
. V skutočnosti však vieme, že feedOne
má byť funkcia, ktorá akceptuje argument typu Animal
a vráti num
. Toto teraz odkomunikujeme Dartu. Najprv definujeme typ pre feeding function:
typedef num Feeder(Animal a);
takto definovaný typ môžme použiť nasledovne:
num feedAll(List<Animal> animals, Feeder feedOne){ //blah
a je to. Opäť, Dart nám na náš typ v podstate kašle, celý typový cirkus je o čitateľnosti a podpore, ktorú dostaneme od DartEditora.
Generics
večný problém. Začnime obligátnym úvodom pre tých, čo ešte nepočuli o:
Kovariancia a kontravatriancia typov
Zabudnime na chvíľu na Dart a vezmime UOOL (Univerzálny Objektovo Orientovaný Lenguidž). Oddeďme z triedy Animal
triedu Cat
a poďme filozofovať. Každá inštancia Cat is Animal
, alebo inak: Cat
ponúka všetky metódy čo Animal
plus dačo navyše. So far so good, v akom vzťahu ale budú List<Cat>
a List<Animal>
? A aby to neznelo ako púha filozofická otázka: Môžme napr. funkcii feedAll
, (očakávajúcej prvý argument typu List<Animal>)
podhodiť parameter typu List<Cat>
?
Na prvý pohľad áno, môžme, to je ale len zdanie. Čo ak by feedAll
vytvorila nový objekt animal
typu Animal
(ale už nie Cat
) a chcela ho pridať do obdržaného listu mačiek? To je akcia zrelá na Exception
! Z pohľadu feedAll
je ale všetko v poriadku (pridáva animal
do List<Animal>
). Celé zle. A aby náhodou nevzniklo zdanie, že sa jedná o collection-only problém:
var catFeeder = (Cat cat) => 0;
je pravda, že catFeeder is Feeder
, resp., mal by byť catFeeder
validným argumentom pre feedAll
(definované vyššie)?
Videl som už viaceré riešenia tohto problému, s každým sa plus mínus dá žiť, ale žiadne ma neokúzlilo. Najzaujímavejšie sa k problému postavila Scala, ktorej autori múdro používajú vedecké pojmy ako kovariantný a kontravariantný, premakali typesystem a všetko čo sa nepodarilo vyriešiť, prehlásili za feature (kto si myslí, že Scala je dokonalá, nech mi napr. skúsi vysvetliť, prečo immutable Set nie je vo svojom type kovariantná).
Generics a Dart
Bez checked módu pracujeme ako v hociakom inom dynamickom jazyku, teda, robíme, čo chceme, pokiaľ neprídeme k problému ako objekt X nemá property Y, všetko bude OK. Pokiaľ zapneme checked mód:
typedef num oneFeeder(Animal);
void main() {
var catFeeder = (Cat cat) => 0;
var sthFeeder = (Something sth) => 0;
print(catFeeder is Feeder); //true
print(sthFeeder is Feeder); //true
print(new List<Cat>() is List<Animal>); //true
print(new List<Object>() is List<Animal>); //false
}
teda, pri typoch funkcií ignorancia typov arguementov, v kolekciách defaultná kovariancia, teda, List<Animal>
je supertype List<Cat>
preto, lebo Animal
je supertype Cat
.
Ako to bolo s tým krájaným chlebom
Dartovský typesystem má niečo do seba. Mám v Darte už čosi odkódené a (zatiaľ) som optimistický: systém pomáha v orientácii v produkčnom kóde, nebrzdí pri prototypovaní, navyše, prakticky nikdy nemusíte bojovať s kompilátorom, aby láskavo pochopil, že váš dobrý kód (z pohľadu dynamického interpretera) je naozaj dobrý (z pohľadu statickej kontroly typov). Väčšinu typových problémov, na ktoré pri písaní Dartovského kódu narazíte, bude mať na svedomí DartEditor, ktorý má v typovej inferencii level UnderBeginner – a má čo doháńať. Pre tých, ktorí rozmýšľajú, čím sa bude treba vyhrážať kolegom/zamestnancom, aby písali do kódu typové anotácie: tým istým, čo používate na to, aby písali dokumentáciu, alebo testy – Dartovské typy sú tak trochu práve týmto.
Feeder by mel byt subtype vsech feederu pro jakykoli konkretni zvire, cili Feeder is CatFeeder a zaroven Feeder is DogFeeder atd. Vyznam „subtype“ relace se obecne da chapat jako zastupitelnost, tzn. subtype se da pouzit vsude, kde je ocekavan supertype. Konkretne u Feederu, obecnejsi Feeder ktery umi nakrmit libovolny zvire, muze byt pouzit kdekoli se ocekava catFeeder, protoze zvlada nakrmit cokoli vcetne kocek. Tim padem Feeder is catFeeder.
Kovariance a kontravariance nejdou zadny vedecky pojmy, i takovy mainstreamovy jazyky jako C# nebo Java podporujou variance annotations pro typovy parametry. Ono by to bez nich ve staticky typovanym jazyce totiz moc dobre neslo.
Jakozta fanouska Scaly by me zajimalo, co konkretne se v typovym systemu Scaly nepodarilo vyresit a bylo prohlaseno za feature. Ze Scala, stejne jako cokoli jinyho, neni dokonala, je jasny. Ale co se tyce zrovna typovyho systemu, tam mi prijde jako jeden z nejdomyslenejsich jazyku vubec vedle Haskellu apod. Proc je Set invariantni je vysvetleno tady http://stackoverflow.com/questions/676615/why-is-scalas-immutable-set-not-covariant-in-its-type, TLDR aby Set mohla byt pouzivana jako funkce z typu prvku do bool. Mozna to neni uplne intuitivni, ale to jeste neznamena, ze to je spatne.
Samozrejme, je to aj otazka chuti. Typesystem pokladam za dobry vtedy, ked co najviac kodov o ktorych viem, ze su spravne (z hladiska pomyselnej dynamickej interpretacie) su dobre aj z hladiska kontroly typov a naopak, ked padajuce kody su oznacene za nespravne. Moja osobna preferencia je: som ochotny akceptovat neodhalenie chyby (kopu tazkych chyb aj tak ziadny typesystem nezachyti), neznasam ale bojovanie s typesystemom, aby laskavo „zozral“ moj spravny kod.
Ako k invariancii ImutSetu pristupi koder, ktory sa snazi funkcii akceptujucej ImutSet podsunut ako parameter ImutSet? Z pohladu kodera je vsetko OK, ale typesystem mu to patricne osladi. Toto tazko predavat ako feature.
Spomenute vysvetlenie poznam, chapem ho vsak skor ako vysvetlenie v style „spravili sme takyto tradeoff, lebo hocico ine by bolo este horsie“.
Vobec netvrdim ze podobne veci robia z typesystemu Scaly nepouzitelny mess, len sa mi nezda dokonaly, to je cele.
Slabá typová inference.
Co to znamená nejdomyšlenější? Třeba bezpečný
printf
tam nenapíšete (počet a typy parametrů závisí na hodnotě prvního argumentu, což Scala neumí popsat).Ano, neni tak silna jak by teoreticky byt mohla (typy parametru funkci, return type rekurzivni funkce), ale nemyslim si, ze by to bylo prohlasovano za feature.
Domyslenosti jsem myslel to, ze neobsahuje ruzne nekonzistence, vyjimky nebo vylozene nesmysly. Existuji i jazyky, ktere jsou na tom jeste lepe a podporuji i vami zminovane dependent types, ale ty povazuju spis za akademicke/experimentalni a rozhodne bych si v nich, narozdil od Scaly, netroufnul vyvijet komercni aplikaci.