Už déle neplatí, že C# je jen okopírovaná Java, a poslední verze Javy převzala od jiných jazyků tzv. lambda výrazy. Jenže trochu podivně.
V C jsou od pradávna pointry na funkci ( void (*funkce)()
) a analogicky (nestandardní) bloky ( void (^funkce)()
). V C++ jsou lambda výrazy také ( function<void()>
). C# dtto.
A jaký typ má lambda výraz (např. obj -> { neco(obj); }
) v Javě? Zde se situace poněkud komplikuje. Každý by asi předpokládal, že to bude objekt. Jenže v Oraclu si přidělali práci a zavedli tzv. funkční rozhraní. A nutí vás je používat. Že jsou některá předdefinovaná v java.util.function
je spíše špatná zpráva než dobrá, protože tam to prostě nepatří.
Dalším problémem jsou proměnné v lexikálním rozsahu platnosti. Překladač si vynutí, aby byly „final“ nebo „effectively final“. Důsledkem je, že proměnné použité v lambda výrazech nelze nijak modifikovat, a to ani vně (sic!) výrazu. Oficiální zdůvodnění je, že ve vícevláknových programech by to dělalo bordel. Jinými slovy – mnozí programátoři jsou nemyslící paka, tak jim to radši rovnou zakážeme. Rozumnější zdůvodnění by bylo (když už teda), že se tím zabrání anomálii vyskytující se v C# (jež se v tomto případě chová jako Javascript), kdy se například proměnná cyklu, v němž nějaký lambda výraz vzniká, „záhadně“ mění pod rukama. Už jsem viděl nesčetně vývojářů, jak se marně snaží najít tuto chybu (nebo „chybu“, on to tak Microsoft udělal schválně). Na pováženou je, že vysvětlení, proč se C# chová, jak se chová, nebyli schopni pochopit.
Jistě, i v C lze s lambda výrazy narazit, např. s následujícím kódem:
__block int p;
int& q = p;
Proměnná q
totiž po nějaké době může ukazovat na náhodné místo v paměti (přesněji: změní se adresa proměnné p
a q
ukazuje stále na stejné místo, kde už nic není nebo tam jsou jiná data). Zdá se, že nejdokonaleji si s výše uvedeným problémem poradilo C++, kde je na odpovědnosti programátora, aby určil, jak se bude proměnná v lambda výrazu chovat: buď si udělá kopii (přístup Javy a defaultně C), nebo použije referenci (přístup Javascriptu a C#).
Shrnuto a podtrženo, lambda výrazy v Javě jsou jen syntaktický cukr. Při použití ve spojení s knihovními metodami (nabízí se hlavně u kolekcí) člověk nemusí vytvářet anonymní třídy, takže ušetří trochu kódu. Zde mají smysl. Pokud je ale nutné stav lambda výrazu nějak měnit, je stále nutné vytvářet si tzv. „holder“, a pak je přehlednější použít anonymní třídu, než mít holder a k tomu ještě funkční rozhraní. Třeba to Oracle v Javě 9 napraví.
No neviem, ci som vsetko pochopil uplne presne, ale predsalen nejake dotazy :
1. lambda ma mat nejaku sablonu, podla ktorej je napisana, nie ? Ak by neboli pouzite tie funkcne rozhrania, tak by neslo o lambdy, ale closures (coz implementuje napr. groovy)
2. nejak mi nie je jasne, co je zle na zamedzeni spominanej anomalie v c#. To mi pride ako spravny postup, ci je nejake spravne vyuzitie tejto 'ficury' ?
3. lambdy su syntaktickym cukrom v kazdom jazyku, ak sa nemylim. Ci vies dat priklad na nieco, co sa v java neda napisat bez lambdy ?
Vždyť to je uzávěr (closure). Lambda výraz je způsob zápisu (syntax, vychází z lambda kalkulu), uzávěr je sémantický pojem. Funkční rozhraní v Javě je jen typ, do nějž se nějaký lambda výraz přiřadí. Je třeba přiznat, že C# se v tomto chová podobně.
"Správné" využití měnitelných proměnných je subjektivní věc, nicméně "stateful" uzávěr může dávat smysl třeba u různých počítadel. Navíc přístup přes referenci je rychlejší (nic se nekopíruje). I když celé to je ještě trochu složitější, protože třeba v ObjC se v obou případech předává reference, jen někdy zvyšuje čítač referencí.
V Javě jde o syntaktický cukr, protože to je funkčně stejné jako anonymní třídy. V C a C++ to syntaktický cukr není právě kvůli možnosti přístupu k vnějším proměnným. Jinak řečeno, Java už uzávěry v podstatě měla (anonymní třídy), kdežto u C/C++ se jedná v novějších verzích o rozšíření funkcionality.
1. Funkcni interfacy byly zavedeny hlavne kvuli zpetne kompatibilite s existujicim kodem, coz je hlavni deviza Javy. Bez te by Oracle Javu mohl rovnou vyhodit. Takze lambda vyrazy pujdou pouzit v milionech radku kodu, kde se pouziva Runnable ruzne ActionListenery, atd. aniz by bylo nutne neco od zakladu preprogramovavat.
2. Omezeni na final je stejne jako u anonymnich trid, takze nic noveho pod sluncem. Navic k funcionalnimu programovani tak nejak prirozene patri to, ze se pracuje s nemenitelnymi objekty.
3. Java je jazyk navrzeny pro softwarovy prumysl, takze omezeni chyb, ktere programator muze udelat je jednim z vychozich cilu toho jazyka. A co si budem nalhavat, hromada programatoru, zejmena tech, co nikdy nevideli funkcionalni program, nechape lexikalni uzaver a jake to ma dusledky. Pokud ma ale nekdo nutkavou touhu masturbovat nad tim, jake uzasne vlastnosti jeho jazyk ma, je tu pro ty ucely C++.
@ded.kenedy To s tou zpětnou kompatibilitou nedává smysl. Runnable by mohlo mít adaptér s konstruktorem pro lambda výraz. Normální návrh syntaxe a knihoven jazyka by tak postupoval místo nějakých kouzel přímo v překladači. Průměrnému vývojáři to může být jedno, s explicitně vyjádřeným typem se nesetká, ale pro psaní vlastních knihoven to je komplikace.
@zboj: Kapitola 2 -- http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html
Tak jsou z tohohle prispevku tedy nejsem moudry, funguje to jako uzavery nebo ne?
@2: Uzavery se hodi treba pro implementaci operaci nad datovymi strukturami. Jelikoz v ramci uzaveru muzes pristupovat k promennym lexikalne definovanym vne uzaveru, chova se to jako kdyby to bylo soucasti tehoz bloku, ale zaroven se ten kod muze provadet v kontextu nejake operace treba nad datovou strukturou, jejiz technicke detaily jsou schovane mimo.
Prikladem muze byt treba funkce, ktera pracuje s kazdym prvkem stromu. Detaily te struktury (treba fakt, ze jde o strom) mohou byt implementovane mimo, jen prislusna metoda vola dalsi parametr, odkaz na funkci (uzaver), ktery pracuje prave s jednim prvkem. Ten pak muze zcela prirozene odkazovat v miste pouziti dalsi promenne.
V Jave se daji uzavery emulovat definici metody ve vnitrni tridy (a proto je to syntakticky cukr), ale urcite to neni syntakticky cukr ve vsech jazycich - treba ruzne Lispy naopak maji uzaver jako primitivu a pomoci ni teprve definuji objekty, jak to krasne popisuje knizka Let Over Lambda.
@zboj: Vyhnout se slozitostem je jednim z hlavnich rysu Javy. Viz muj komentar 3.3.
@JS: Je to uzaver, jen promenne, ktere jsou deklarovane mimo danou funkci, a ke kterym se pristupuje, musi byt final, stejne jako u anonymnich trid.
To s tema Lispama je dobra poznamka. Zatim co v Lispech se pomoci funkci vytvari objekty, v Jave se pomoci objektu vytvari funkce.
Lambdy nejsou jen syntaktický cukr nad anonymními třídami, kompilují se jinak. Zatímco anonymní třída vždycky vede na standardní class soubor (ala Foo$1.class), tak lambda vede na invokedynamic instrukci, kterou může JVM realizovat v podstatě jak se jí chce.
To má za následek několik věcí: zaprvé méně bytekódu a menší využití permgenu (resp. meta spacu nebo jak se ta jeho náhrada teď jmenuje). A za druhé, pokud je daná lambda stateless a nezachytává žádné lokální proměnné z vnějšího scopu, může pro ni JVM vygenerovat obyčejnou statickou metodu, což je nejen opět rychlejší a úspornější z hlediska paměti, ale i zabraňuje ne moc očividným, ale o to nepříjemnějším problémům, které lze anonymními třídami způsobit.
Definujeme-li anonymní (či lokální třídu) v instanční metodě, bude mít její instance implicitní referenci na objekt obalující třídy. Představte si, že v nějakém GUI dialogu si vytvoříte anonymní Runnable, ten někam pošlete do fronty, a okno dialogu zavřete. Nu, dokud se daný Runnable neprovede a nezahodí, tak bude držet referenci na tento dialog, který tím pádem nebude moc být uklizen pomocí GC. Memory leak jak vyšitý. O obdobné situaci, kdy vytvoříme anonymní subclassu nějaké serializovatelné třídy, radši ani nemluvě.
Ad [4]:
Nedokážu si představit, jak by takový Runnable "adaptér" měl vypadat. Můžete to rozvést?
@Natix To není memory leak, po vyřazení z fronty by se vše uvolnilo. Nicméně k věci - v tomto případě by lambda výraz byl ekvivalentní statické vnitřní třídě, ta taky nezachytává this vnější třídy. Trochu to zde připomíná C++, kde za určitých podmínek lze přetypovávat mezi funktory a pointry na funkci.
[12] Dobrá, blbý příklad, ale pokud by to byl listener, který by žil klidně celou dobu běhu aplikace, tak by to leak byl. Příkladů by se našlo určitě spousta.
Použití statické vnitřní třídy je samozřejmě naprosto správné řešení tohoto problému, ale málokdo toto ví, a ten kdo to ví, ten si to pro změnu neuvědomí...
@Natix Jo, listenery jsou v tomto směru problém (nejen v Javě). Například v ObjC se deregistrují v dealloc. V jazycích s GC se to musí řešit extra.
A to "málokdo to ví" - to je bohužel všudypřítomný problém, že lidi znají základ jazyka/knihovny, ale nuance jim unikají. V tomto směru jsou lambda výrazy určitě přínosem, byť stále jsem toho názoru, že programátoři by měli znát jazyk, v němž píší, do hloubky, včetně aspektů, o které se typicky stará implicitně překladač nebo knihovna.
@zboj: "jsem toho názoru, že programátoři by měli znát jazyk, v němž píší, do hloubky, včetně aspektů, o které se typicky stará implicitně překladač nebo knihovna"
Tento nazor je zvraceny. Programovaci jazyk je nastroj a programator tu neni od toho, aby se zabyval programovacim jazykem, ale aby resil realne problemy.
[16] +1 Naprostý souhlas. Kolikrát jsem tohle už řešil, bohužel poměr takových programátorů bohužel u Javy roste na úkor těch, kteří mají jasnou představu, jaký kód procesor skutečně zpracovává. Na tyhle věci často narážím u aplikačních serverů pod větší zátěží, najednou jsou tam problémy se synchronizacemi, memory leaky a vůbec věcma kolem výkonu. Minimálně by se měli lidé naučit koukat na své aplikace skrze debugger, prokrokovat si svůj kód a podívat se, co všechno ještě mají na stacku a zda to tam opravdu chtěli mít (bordel "díky" špatně nastaveným aspektům apd.).
Těším se na Stream API, které zpřehlední spoustu kódu v těchto projektech realizujícího obchodní logiku, perfektně to doplňuje objektové modely mapované na messaging či perzistenci. Ale pozor, tohle má už nádech nejen funkčního, ale také deklarativního programování. Pokládám za nutné, aby programátor znal mechanismy za tím, jinak dopadneme jako u SQL, kde každý noob dokáže napsat výraz, ale pak se diví, proč některé nefungují, přestože to přece tak pěkně napsal. SUN na tohle nabízel řešení, certifikace SCJP byla velice kvalitní a podrobná, donutila programátora nahlédnout, mít povědomí o těchto věcech, doufám, že Oracle dokáže udržet laťku vysoko.
Problémem C#, Javy a JavaScriptu je to, jak tam fungují proměnné - to komplikuje lambda výrazy.
@17 "kteří mají jasnou představu, jaký kód procesor skutečně zpracovává" - To je pro Javu přesně specifikované?
@17 "Minimálně by se měli lidé naučit koukat na své aplikace skrze debugger, prokrokovat si svůj kód a podívat se, co všechno ještě mají na stacku a zda to tam opravdu chtěli mít" - Minimálně?
@21 Nekomplikuje. Zrovna C# a Java se v implementaci lambda výrazů liší. V C# se prostě pracuje přímo s referencí na haldu, kdežto v Javě a (defaultně) C se dělá kopie, kterou si drží instance bloku. Složitější je to v C v případě použití __block, kdy se kopíruje proměnná ze zásobníku na haldu, pokud se kopíruje blok ze zásobníku na haldu.
@21 Dobrá poznámka k JVM, spousta javistů ráda pracuje s abstrakcí v podobě javy a nechtějí se pouštět do problematiky běhu nativního kódu na procesoru, jenže právě v těchto případech se pak hrozně diví, když se setkají s pády JVM. Tyhle pády se třeba na těch aplikačních serverech pod zátěží občas objevují a jsou odlišné dle implementace JVM. Třeba Oracle JRockit JVM se chová ve spoustě věcí hodně odlišně. Ale i když zůstanu u té abstrakce v podobě svět končí na JVM vrstvě, veledůležité je třeba znát dobře chování classloaderů i třeba typy garbage collectorů, servery jich využívají více typů, je dobré vědět kdy jaký použít a být si vědom omezení, která způsobují. Debugger by se měl používat na více věcí než jen krokovat svůj kód lokálně i vzdáleně, jde třeba o úpravu kontextu pro simulaci různých výjimečných situací na živém serveru, ono je něco jiného mít fungující unit testy a něco jiného živou instanci na serveru s mnoha naloadovanými knihovnami. Závěrem připomenu profilery, prakticky v každé aplikace se najde pomocí profileru místo, které je překvapivě neefektivní, profiler tak nepomáhá jen přímo zvýšit výkon aplikace, ale také najít logické chyby v kódu nebo nehospodárné nakládání s externími prostředky (typicky tak člověk přijde na frekventovaná volání databáze, která jsou buď zbytečná nebo se je dá omezit použitím cache, podobně je to s XML operacemi, kompilováním regulárních výrazů, lazy loadingem apd.).
Zapoměl jsem ještě na jednu důležitou pomůcku, tou je dekompilace. Pomáhá pochopit některé mechanismy, které při běžném zíraní do zdrojového kódu nejsou vidět a člověk jen kroutí hlavou, jak tady může dojít k null pointer výjimce? Já vím, že je to spousta věcí, ale opravdu je dobré je znát, vývojáři vědomí si těchto věcí píší kód jinak, je to pěkně vidět přímo ma zdrojovém kódu javy, springu a dalších známých knihoven, i tohle je dobrá inspirace, často si při kafíčku místo čtení zpráv pročítám zdrojové kódy knihoven co používáme a hledám poklady :)
@22 Potíž je v tom, že proměnné v těchto programovacíh jazycích jsou něco úplně jiného než proměnné v lambda kalkulu. Význam proměnných v těchto jazycích není určen substitucí, ale přiřazovacím příkazem. Důsledkem je, že se o programech v těchto jazycích hůře přemýšlí - nejde totiž použít substituční model.
Autor se zabývá vývojem kompilátorů a knihoven pro objektově-orientované programovací jazyky.
Přečteno 36 207×
Přečteno 25 364×
Přečteno 23 797×
Přečteno 20 179×
Přečteno 17 876×