Internet Info, s.r.o. Lupa Root Měšec Podnikatel DigiZone Slunečnice Vitalianew Bomba Navrcholu Weblogy Jagg Woko Dobrý web Computer.cz SK: MojeLinky
Root.czBlogyZmatení (programovacích) jazyků

Přirozený jazyk a bezkontextová syntax

zboj, 18. 02. 2012, 19:00 v kategorii Zpracování přirozeného jazyka,

Zpracování přirozeného jazyka patří k nejsložitějším algoritmickým úlohám. V přirozených jazycích se střetává tendence vyjádřit co nejvíce co nejpřesněji s ekonomickým principem, tedy tendencí slovní vyjádření redukovat (neformálně můžeme mluvit o lenosti). Lidské jazyky jsou proto (narozdíl od formálních) víceznačné na všech úrovních, které jazykověda rozlišuje (tvarosloví, syntax, sémantika), čehož důsledkem jsou problémy s formálním popisem jazyka a jeho strojovým zpracováním.

Když Noam Chomsky představil vědecké obci své formální gramatiky (předtím byla formální jazykověda výrazně strukturální nebo funkcionální), znamenalo to malou revoluci (kromě jazykovědy i v informatice). Chomsky se při popisu jazyka (téměř výlučně angličtiny) zaměřil na gramatiky bezkontextové, byť se záhy ukázalo, že většina jazyků bezkontextová není. Navíc Kenneth Hale ukázal, že mnohé jazyky nejsou konfigurační (včetně jazyků slovanských, tedy i češtiny), a složková (frázová) syntax tak zachycuje pouze slovosled.

Z Chomského generativní gramatiky vyšlo mnoho moderních teorií popisu jazyka. V dalším článku se zaměřím na LFG (Lexical Functional Grammar), v tomto na X'-teorii (čti: X bar theory). Jako českou kuriozitu lze zmínit Funkční generativní popis prof. Sgalla, o tom ale jindy.

X'-teorie vychází z předpokladu, že věty a fráze mají téměř vždy hlavu, tj. element, jenž nějakým způsobem dominuje zbytek fráze. Ve jmenných frázích je touto hlavou podstatné jméno, ve větách sloveso (finitní) apod. Zavádí proto úrovně neterminálů, které označuje znakem ' (proto X'). Maximální projekce neterminálu X se označuje XP (ekvivalent k X'', obvykle je nejvyšší úrovní ta třetí). Bezkontextová pravidla jsou pak omezena na typ

Xn+1 → Xn YP

a

XP → YP Xmax-1

přičemž YP v prvním pravidle se nazývá komplement a v druhém specifikátor (angl. specifier). Projekce kategorie X je pak hlavou pravidla.

Je zřejmé, že se jedná o běžnou bezkontextovou gramatiku s obyčejnými neterminály, jejíž pravidla jsou však pouze specifického typu. Stále se jedná o způsob zápisu jazyků orientovaný na angličtinu, a tedy nevhodný pro nekonfigurační jazyky. O vhodnějším modelu pro mnohem širší spektrum jazyků (a jeho implementaci v C++) přístě.

Všudypřítomná harmonie

zboj, 11. 01. 2012, 18:15 v kategorii Programovací jazyky,

K nejrozšířenějším jazykům dneška patří JavaScript (JS). Navzdory svému jméno se od Javy dost liší, sdílí s ní v podstatě jen céčkovou syntax. Byl kodifikován pod názvem ECMAScript a nedávno jsme se dočkali verze 5,1.

Verze 5,1 je jen mírným pokrokem, daleko větším měl být JS 4,0, jejž ovšem Microsoft jako normu zablokoval (nelíbilo se mu, že by se Adobe prosadilo se svým ActionScriptem). Nakonec se všichni velcí hráči shodli (a podle této shody novou verzi pojmenovali Harmony), nicméně oficiální posvěcení ve formě kodifikace stále chybí a implementace v prohlížečích je v nedohlednu.

Harmony přináší třídy (a odpovídající "neprototypovou" dědičnost), jmenné prostory, typovou kontrolu atd. To vše už tu jako návrh (JS4) nějakou dobu bylo, takže existuje pár více či méně tuto specifikaci naplňujících implementací.

Flash

Adobe se Harmony inspirovalo při návrhu ActionScriptu. Ve Flashi tak máte k dispozici plnohodnotný jazyk založený na virtuálním stroji s mnoha knihovnami.

.NET

Microsoft pro svůj .NET vytvořil překladač JScript.NET pro JS se syntaxí podobnou Harmony. Tento překladač generuje bajtkód (MSIL), takže v něm napíšete téměř vše, co v C#. Především lze využívat BCL. Bohužel vývoj se zastavil u verze .NET 2,0.

Prohlížeč

Prohlížeče Harmony nativně nepodporují, existují ale nástroje pro překlad do "prostého" JS. Jedním z nich je Google Traceur, dalším například Mascara. Celé to funguje jako GWT (které překládá z Javy) a podobné transpilery. Narozdíl od JScript.NET není k dispozici standardní knihovna, jste odkázáni na API třetích stran (případně CommonJS).

Vazba na C a Javu

V céčku či Javě lze JS spouštět pomocí knihoven. Pro Javu se používá především Rhino, pro C/C++ můžete použít V8 (překládá JS do nativního kódu) nebo JavaScriptCore (podle verze interpretuje, překládá do bajtkódu nebo do kódu procesoru). Sice neumí nativně Harmony, ale "transpilovaný" kód běží bez problémů. Tento postup využívá například Titanium pro běh aplikací na Androidu a iOS.

Frázové a abstraktní syntaktické stromy

zboj, 2. 01. 2012, 19:09 v kategorii Nezařazené,

Pod článkem o Jisonu se rozvinula zajímavá diskuse, navážu proto na tématiku stručným vysvětlením rozdílu mezi abstraktní a frázovou syntaxí.

Nejprve je třeba ujasnit si terminologii, která je tu mírně zmatená. Frázové stromy se tak nazývají podle frázové gramatiky, která se definuje pro každý formální jazyk. V literatuře je najdete také pod názvem "konkrétní" stromy (jako protiklad k abstraktním). Abstraktní syntaktické stromy (anglicky Abstract Syntax Tree, odtud často používaná zkratka AST) se odvozují z frázových a lze je přímo použít pro interpretaci jazyka (nebo kompilaci).

Parsingu zpravidla předchází lexikální analýza, jejímž výstupem je seznam tokenů (u programovacích jazyků bývá gramatika jednoznačná). Počet tokenů je stejný jako počet listů frázového stromu. Listy jsou ohodnoceny vstupními tokeny, ostatní vrcholy stromu jsou ohodnoceny neterminály gramatiky, na jejímž základě byl strom vytvořen.

Abstraktní strom obsahuje menší počet vrcholů, je roven nanejvýš počtu tokenů. Převod z frázového na abstraktní strom je plně automatický a není ovlivněn sémantikou jazyka. Každému frázovému stromu přitom odpovídá právě jeden abstraktní, opačně to ale samozřejmě neplatí.

S přihlédnutím k sémantice lze definovat ekvivalenci abstraktních stromů, např. vzhledem k vlastnostem operace sčítání jsou abstraktní stromy pro výrazy (a+b)+c a a+(b+c) ekvivalentní, pro mínus to už ale neplatí. Frázový strom těchto výrazů bude mít 10 vrcholů, abstraktní 5.

Při převodu na abstraktní strom se určuje hlava pravidla, což je většinou operátor nebo jméno funkce (u volání funkce). V příkladu výše by to byl operátor +.

V závislosti na sémantice se abstraktní stromy po parsingu ve fázi optimalizace upravují (některé podstromy např. nejsou potřebné). Interpretace abstraktního stromu je přímočará (fungují tak interprety Javascriptu), při kompilace se používá většinou mezikód, a to buď explicitní (např. bajtkód v .NETu či Javě), nebo implicitní (vnitřní reprezentace kódu, jako např. v LLVM).

Frázové stromy při implementaci interpretu nebo překladače v podstatě nikdy nevidíme, jsou jen jakousi imaginární datovou strukturou, již generuje gramatika, vlastní parser generuje přímo AST. Navíc často splývá syntax a sémantika. Pokud si chcete vyzkoušet, jak se netriviální gramatika implemetuje, doporučuji třeba ANTLR.

Postavte si jazyk

zboj, 21. 12. 2011, 00:16 v kategorii Pokročilé techniky,

Vývojáři jsou lid kreativní, a jak lépe vybít svou kreativitu, než vytvořením vlastního jazyka? Teď nechci nabádat k vynalézání kola, sám jsem vymyslel v životě kdysi dávno jen jeden jazyk, a to jen proto, že to bylo nutnou podmínkou pro zápočet, všechny ostatní překladače, které jsem kdy napsal, měly za cíl jazyky již existující a nanejvýš mírně upravené. Zde stručně popíšu, jak jednoduše napsat vlastní parser a s ním interpret nebo transpiler.

Nemusíte vědět, kdo je Noam Chomsky, ale měli byste tušit, co je formální gramatika. Regulární jazyky zná asi každý, kdo programuje, tak vězte, že bezkontextové gramatiky jsou něco podobného, jen mají větší generativní sílu. Profíci používají flex a yacc nebo bison či podobné nástroje. Mnohem snazší je ale začít s nějakým skriptovacím jazykem. Vezměme Javascript (JS).

Javascript bez prohlížeče

Kopií bisonu pro JS je jison (snadno vygooglíte). Následuje jednoduchá gramatika pro aritmetické výrazy.

var Parser = require("jison").Parser;
var grammar = {
"lex": {
"rules": [
["\\s+", "/* skip whitespace */"],
["[0-9]+(?:\\.[0-9]+)?\\b", "return 'NUMBER';"],
["\\*", "return '*';"],
["\\/", "return '/';"],
["-", "return '-';"],
["\\+", "return '+';"],
["\\^", "return '^';"],
["\\(", "return '(';"],
["\\)", "return ')';"],
["PI\\b", "return 'PI';"],
["E\\b", "return 'E';"],
["$", "return 'EOF';"]
]
},

"operators": [
["left", "+", "-"],
["left", "*", "/"],
["left", "^"],
["left", "UMINUS"]
],

"bnf": {
"expressions": [["e EOF", "return $1;"]],

"e": [["e + e", "$$ = $1 + $3;"],
["e - e", "$$ = $1 - $3;"],
["e * e", "$$ = $1 * $3;"],
["e / e", "$$ = $1 / $3;"],
["e ^ e", "$$ = Math.pow($1, $3);"],
["- e", "$$ = -$2;", { "prec": "UMINUS"}],
["( e )", "$$ = $2;"],
["NUMBER", "$$ = Number(yytext);"],
["E", "$$ = Math.E;"],
["PI", "$$ = Math.PI;"]]
}
};

První řádek předpokládá, že již máte nainstalovaný jison. Ten funguje například s narwhalem (pomalý, nedoporučuji) nebo s nodejs (to je interpret JS postavený nad superrychlým V8 od Googlu, opět snadno vygooglíte). Gramatiku můžete velice jednoduše interpretovat takto:

var parser = new Parser(grammar);
var result = parser.parse("2 + 3");
console.log(result);

Teď máme prakticky zadarmo interpret, možná si ale říkáte, že nutnost mít nodejs (nebo něco podobného) je svazující. To je v podstatě pravda, ale jakmile gramatiku odladíte, není nic snazšího než použít metodu generate:

parser.generate();

Výslednému kódu sice asi nebudete rozumět, ale podstatné je, že se tak zbavíte závislostí na nodejs. Vygenerovaný kód v JS bez problémů interpretuje libovolný moderní prohlížeč, dokonce i MSIE 6 (ten ovšem trošku pomaleji).

Parsing mimo prohlížeč

Kromě prohlížečů můžete kód použít i v nativní aplikaci. Jednou z možností je využít V8, což je interpret JS od Google, zadarmo a rychlý. Vlastně ani ne interpret, většina kódu se totiž překládá do nativního kódu pro Intel nebo ARM. V8 můžete ke své aplikaci staticky přilinkovat (osobně vyzkoušeno pro iOS), nevýhodou je jen větší velikost binárky.

V aplikacích pro Windows můžete využít JScript.NET. K vygenerovanému kódu přidáte toto:

import System;
package ParserDemo {
public class Parser {
public function Parse(s:String) {
return parser.parse(s);
}
}
var p = new ParserDemo.Parser();
Console.WriteLine(p.Parse("2 * 3"));

Překladač jsc vygeneruje spustitelný soubor, můžete také vygenerovat dynamickou knihovnu (DLL) a tu volat z kódu v C#, VB, C++/CLI atd. V tomto případě můžete použít volbu /fast+, což znamená, že výsledkem překladu bude čistý MSIL (CIL) kód, tedy žádný interpretovaný JS, ale plnohodnotný bajtkód, jejž JIT před spuštěním přeloží do nativního kódu.

Jak vidíte, napsat si jednoduchý interpret nebo transpiler není vůbec složité. Do nástrojů jako Google Web Toolkit (GWT) to má sice prozatím daleko, ale první krok bychom již měli.

IAsyncOperation<WTF>

zboj, 16. 12. 2011, 13:42 v kategorii Programovací jazyky,

Jak ve WinRT získáte seznam souborů v adresáři? V C# nějak takto:

var folder = KnownFolders.PicturesLibrary;
var files = await folder.GetFilesAsync();
foreach (StorageFile file in files) { /* ... */ }

A jak to bude vypadat v C++/CX?

auto folder = KnownFolders::PicturesLibrary;
auto op = folder->GetFilesAsync();
op->Completed = ref new AsyncOperationCompletedHandler<IVectorView<IStorageFile^>^>(
[](IAsyncOperation<IVectorView<IStorageFile^>^> op) {
if (op->Status == AsyncStatus::Completed) {
auto files = op->GetResults();
/* ... */
}
});
op->Start();

Zdá se mi, že soudruzi v Redmondu by měli ještě trochu zapracovat na své verzi C++, než vypustí první betaverzi.

Jak (ne)optimalizovat spotřebu paměti v .NET

zboj, 13. 12. 2011, 23:13 v kategorii Pokročilé techniky,

Dnes jen krátce a ke konkrétnímu tématu. Jak známo, velké objekty GC na haldě nepřesouvá, takže pokud si je budete alokovat a uvolňovat sami, nezpůsobíte svým počínáním větší fragmentaci než GC. Právě velké objekty je vhodné uvolnit co nejdříve a pokud možno deterministicky. To není v .NET problém, na neřízené haldě lze alokovat třeba i sto MB a díky IDisposable paměť uvolnit, jakmile objekt nepotřebujeme. Můžeme mít tedy ve třídě nějaký pointer:

private: Neco* ptr;

Ten alokujeme v konstruktoru a uvolňujeme v destruktoru, tedy Dispose:

~Trida() { delete ptr; ptr = nullptr; }

Potíže nastanou, pokud nedisciplinovaný programátor objekt explicitně nezruší. Na první pohled vhodné řešení s finalizérem může být fatální:

!Trida() { delete ptr; }

Tím sice teoreticky zamezíme únikům paměti, jenže GC na neřízenou haldu zvysoka, ehm, kašle, takže dříve než úklidu se dočkáte výjimky OutOfMemoryException. Mnohem lepší je vyhodit ve finalizéru výjimku:

!Trida() { throw gcnew Exception("Object not disposed properly"); }

Nezlikvidovat náležitě objekt je pochopitelně chyba programátora, ale pořád se najdou jedinci, kteří nečtou dokumentaci, případně v životě neslyšeli o using, takže naservírováním výjimky, která s přehledem odstřelí celou aplikaci, jim vlastně děláte laskavost.

.NET aneb cesta tam a zase zpátky

zboj, 12. 12. 2011, 12:23 v kategorii Nezařazené,

Windows vznikaly od počátku v jazyce C, je proto logické, že i rozhraní pro psaní aplikací bylo (a dodnes je) v céčku. Toto Windows API, později nazývané Win16, nebylo nijak extra dobře navržené, ale protože Microsoft vždy kladl důraz na zpětnou kompatibilitu, vydrželo (v rozšířené formě nazývané Win32) až do dnešních Windows 7 (a jeho podmnožinu najdeme i v Metru).

COM, DCOM, COM+

S rozmachem objektově orientovaného programování začaly vznikat různé nadstavby i nad WinAPI, vesměs v C++. V 90. letech navíc začaly vznikat rozsáhlé objektové systémy založené na komunikaci mezi nezávislými komponentami, v podstatě každá důležitá IT firma měla svůj: Sun měl NEO (přejmenované DOE=Distributed Objects Everywhere), v IBM měli SOM (System Object Model), NeXTStep vytvořil PDO (Portable Distributed Objects) atd. Odpovědí Microsoftu na tento trend byl Component Object Model (COM).

Stejně jako se zkušení vývojáři smáli složitosti kódu pro "hello world" ve WinAPI, dělali nyní to samé při porovnání COM a PDO, protože v prvně jmenovaném byl kód se stejnou funkčností cca. 10x větší. Dělat s COM bylo vskutku složité a i když později to usnadňovaly některé vývojové nástroje, nikomu tato technologie asi k srdci nepřirostla.

Variantou COM byl DCOM (distribuovaný COM) a nakonec COM+ spojující obě sesterské technologie.

.NET

Když soud zatrhnul Microsoftu upravit si Javu, pustili se v Redmondu do vývoje vlastního virtuálního stroje a jazyka nad ním. V roce 2002 představený .NET s novým C# a knihovnou BCL byly ve své první verzi jen obyčejnou kopií Javy, nicméně v následujících deseti letech byl vývoj značně dynamičtější než u Javy a dnes je Java minimálně o generaci pozadu.

Na přelomu tisíciletí se obecně predpokládalo, že .NET ve Windows nahradí WinAPI a jednoho krásného dne bude i jádro Windows běžet v řízeném kódu (takový experimentální systém v MS skutečně existuje). Bohužel (nebo bohudík) i Windows 7 jsou založené na WinAPI a od MS téměř žádné aplikace v .NET dodnes nemáme. Jiní vývojáři sice občas s něčím v .NET přijdou, ale problémy s instalací runtimu spolehlivě odrazují od .NET, má-li být výslednou aplikaci schopen nainstalovat každý běžný uživatel (o jejichž inteligenci se tu asi rozepisovat nemusím).

Azure a Windows Phone 7

Postavit Azure nad .NET bylo jistě velmi snadné rozhodnutí, Ballmer teď může tvrdit, že .NET je úspěšným projektem, protože na něm jede v rámci Azure spousta aplikací.Povolil tak tlak na WinDiv, jež vždy tíhla k nativnímu kódu, aby více využívala .NET, a zároveň má DevDiv (jež vyvíjí .NET a věci kolem) záminku k existenci.

Proti Flashi se snažil MS bojovat také pomocí .NET a vyvinul Silverlight. Ten se sice na webu neujal, ale našel uplatnění alespoň jako runtime pro Windows Phone 7. Relativní úspěch Azure se ale asi opakovat nebude, neboť ve Windows Phone 8 bude .NET nahrazenen novým WinRT.

WinRT a "nové" C++

S trochou nadsázky se dá říci, že .NET měl být jednodušší a modernější náhradou za COM. Svým způsobem i byl a v serverovém prostředí bychom těžko hledali lepší konkurenci k Javě. Jenže dnešním trendem jsou mobilní technologie a jak v MS veřejně připouští, .NET se na relativně pomalá zařízení s omezeným množstvím paměti nehodí.

Nástupcem zastaralého WinAPI má být WinRT (jak jsem už zmínil, před několika lety to měl být .NET). WinRT je zbrusu nové jádro s API postaveným na, a teď pozor, staré dobré technologii COM. Největší nevýhodu COM, složitost pro vývojáře, má odstranit "nové" C++ (píšu v uvozovkách, protože skutečné nové C++ je pro mě C++11), označované dříve WinC++ a nyní C++/CX. Jde o C++ s rozšířenou syntaxí pro COM komponenty (teď se ovšem nazývají WinRT komponenty). Z .NET se do WinRT dostal pouze formát metadat popisující rozhraní komponent. Existuje sice tzv. projekce nového WinRT do C# a VB, jenže jejich knihovna (BCL) je natolik omezená, že existující kód pro .NET bez úprav nepřeložíte.

Microsoft tak během deseti let udělal zajímavé kolečko. Opustil COM, vyvinul .NET a věci kolem, uplatnil jej v serverových produktech a, paradoxně, v operačním systému pro mobily, aby se nakonec pokorně vrátil k nativnímu a jen mírně modernizovanému COM. Nabízí se srovnání s Applem, který šel místo několika veletočů cestou evoluce a od koupě OpenStepu v roce 1996 pracuje stále se stejnou technologií, již postupně modernizuje. Místo bajtkódu se používají "tlusté" binárky (fat binary), takže lze jeden soubor spustit na různých procesorech (dříve PowerPC a Intel, dnes to samé používají pro ARMv6 a ARMv7), runtime sice nezpracovává bajtkód, ale nemá žádnou nevýhodu oproti VM s JIT, správa paměti je pro programátora stejně nenáročná jako v .NET atd. Buďme rádi, že je Microsoft (nebo aspoň jeho akcionáři) schopen sebereflexe a vrací se k osvědčené technologii.

Jak chce Microsoft zvýšit výkon Windows 8

zboj, 8. 12. 2011, 12:26 v kategorii Nezařazené,

V souvislosti s nedávným představením Windows Store se opět píše o Windows 8. V únoru má být k dispozici beta verze a vývojářům bude umožněno nabízet v novém obchodě aplikace (v beta verzi zatím pouze zadarmo). Protože jádro Windows 8 je zcela překopané, neuškodí shrnout si některé poznatky o novém systému. Nebudu psát o novém UI (Metro), jež je věcí osobního vkusu. Zaměřím se na to, jak chce Microsoft zvýšit výkon aplikací běžících nad novým Windows Runtime (WinRT).

Nativní kód

Staré, ale stále používané Win32 API má nahradit WinRT. Jedná se o objektově orientované API napsané v C++. Rozhodování mezi Win32 a .NET dopadlo stejně, jak kdysi církev vyřešila problém dvou papežů: zvolili třetího.

WinRT je založené na technologii COM, již už všichni považovali za překonanou. V případě komponent z WinRT API (a komponent třetích stran) se jedná o staré dobré COM objekty zkřížené s metadaty z .NET. Tato metadata (pro účely WinRT je překladač zapisuje do souborů s příponou winmd) umožňují využití nativních komponent z řízeného kódu mnohem efektivněji než přes P/Invoke.

Vývojářům nabízí Microsoft pro Metro API v C++. Nebyl by to ale Microsoft, aby si nevymyslel něco extra. Vzniklo tak C++/CX (CX=Component Extensions), které rozšiřuje syntax C++ právě o ony renovované COM komponenty. Syntax je z větší části převzata z C++/CLI (pokud nesnášíte nestandardní rozšíření, můžete psát v čistém C++ s využitím knihovny WRL).

Dobrou zprávou je, že Visual Studio 2011 (současné preview) podporuje velkou část nového standardu C++11. Oficiální stanovisko Microsoftu je, že vývojáři by měli psát svůj kód v čistém C++ a rozšířenou syntax používat je pro komunikaci s novým API nebo s UI (které se definuje v již používaném deklarativním jazyce XAML).

Paměť

Jak na svém blogu sdělil Steve Sinofsky, Microsoft došel po "rozsáhlém výzkumu" k závěru, že v telefonech a tabletech je nejlepší mít co nejméně paměti. Ne že by to zrychlilo systém či aplikace, ale šetří se tak baterie. Celé WinRT je proto navržené tak, aby pamětí neplýtvalo. Předně nemá garbage collector. To lze samozřejmě obejít (UI lze napsat v .NETu nebo Javascriptu), ale narozdíl od CLR není GC součástí runtimu.

Asynchronní API

Náročnější operace se dají ve WinRT provádět pouze asynchronně (např. práce se soubory). Nové rozhraní IAsyncOperation<> se přitom stará o předávání výsledků. C# nově dostalo klíčová slova async a await, aby mohli vývojáři psát vlastní asynchronní metody způsobem, na který jsou zvyklí (sekvenčně).

Pokud přesně nevíte, o co jde, doporučuji podívat se na NodeJS a jeho způsob práce s callbacky. Trochu zamrzí, že Microsoft ignoroval možnosti, které poskytuje nové C++11 (např. std::future) a vytvořil si vlastní, nekompatibilní API.

Jak je na tom .NET?

.NET se ve Windows 8 dostává na vedlejší kolej, ale pořád je možné jej volitelně využívat (CLR může běžet nad WinRT, ale standardní API, tedy knihovna BCL, je osekané). Pokud máte hodně kódu, bude snadné upravit jej (pokud se netýká UI) pro .NET v Metru (úplně beze změn to bohužel většinou nepůjde).

Předpokládám, že Microsoft napíše nový JIT překladač pro procesory ARM, protože v současné době běží .NET na ARM neuvěřitelně pomalu, zatímco na Intelu řádově stejně rychle jako nativní kód. Jenže .NET Micro Framework je okrajová záležitost, kdežto na Windows 8 sází MS svou budoucnost. Doufám, že to dopadne lépe než na Androidu, kde podle vyjádření společnosti ARM běží nativní kód vygenerovaný jejich vlastním překladačem 4x rychleji než kód nad Dalvik VM.

NB: Že to celé funguje, dokazuje preview verze Win8, která bootuje podstatně rychleji než Windows 7 (ovšem zatím jí toho dost chybí) a vystačí si s podstatně menším množstvím operační paměti.

Rozdíly v lambda výrazech v různých jazycích

zboj, 7. 12. 2011, 10:50 v kategorii Pokročilé techniky,

Podle posledního indexu Tiobe jsou nejpopulárnějšími jazyky Java, C, C++, C# a Objective-C. Je zajímavé porovnat implementaci a omezení lambda výrazů v těchto jazycích, člověk tak snáze porozumí, jak to uvnitř funguje.

Java

Java lambda výrazy nemá (existuje experimentální implementace, objevit se mají v osmé verzi). Má ale anonymní třídy, které je snadno nahradí. S tím se pojí určitá omezení. Chceme-li použít lokální proměnnou uvnitř lambda výrazu, musí být deklarována jako final:

final int n = 0;
new Trida() { public int inc() { return n + 1; }}

Z toho samozřejmě plyne, že takovou proměnnou nemůžeme v anonymní třídě měnit. Toto omezení je dáno implementací, anonymní třídy totiž mohou přežít kontext, v němž byly vytvořeny, tedy po zániku proměnných na zásobníku. Díky final může runtime vytvořit kopii bez nebezpečí, že se data později změní. Pokud potřebujeme mít modifikovatelnou proměnnou, deklarujeme ji jako instanční proměnnou buď v anonymní třídě, nebo v třídě, v jejíž metodě jsme anonymní třídu vytvořili.

Pěkným příkladem použití anonymních tříd je AsyncCallback v GWT.

C

Podporu lambda výrazů má C v clangu (tam se nazývají bloky). Každý blok (zapsaný literálem) se vytvoří na zásobníku a se zásobníkem také zanikne. Pokud chceme blok zachovat (např. předat jako callback asynchronní metodě), musíme jej zkopírovat na haldu pomocí Block_copy (z block.h). Tím přebíráme odpovědnost za jeho odstranění z haldy funkcí Block_release.

Lokální proměnné se kopírují. Pokud chceme nějakou měnit, musí být deklarována pomocí __block. Takto deklarované proměnné jsou uloženy v objektu, který vnitřně reprezentuje blok. Zkopírujeme-li blok na haldu, tyto proměnné se zkopírují s ním. Zde je jistá podobnost s C# (lokální proměnná na haldě), narozdíl od C++ (viz dále) můžeme k modifikovatelným proměnným přistupovat i mimo kontext, v němž byl blok vytvořen.

C++

V clangu můžeme používat bloky z C (viz výše). Instance tříd se pro přístup z bloku kopírují (pokud nepoužijeme __block). V C++ se můžeme odpovědnosti za správu bloku na haldě zbavit jednoduchým wrapperem, tj. třídou, která přetíží operátor () a automaticky volá Block_copy a Block_release (taková třída bude podobná chytrým ukazatelům, Block_copy totiž kopíruje jen ze zásobníku na haldu, na haldě jen bloku zvýší čítač referencí, Block_release jej naopak snižuje, takže blok zanikne, až na něj neexistuje žádný odkaz). Takový wrapper má navíc tu výhodu, že je typu std::function<>, je tedy použitelný v std:.thread, std::async apod. Clangovské bloky lze přímo použít např. v Grand Central Dispatch, ale ne v STL.

Lambda výrazy v C++11 mají jinou syntaxi i implementaci. Jsou v podstatě jen syntaktickým cukrem nad funktory. Každá proměnná, kterou chceme ve výrazu použít, musí být explicitně uvedena (v "capture" seznamu). Proměnná se buď zkopíruje (a pak ji můžeme použít i mimo kontext, v němž byl výraz vytvořen) nebo se na ni odkazuje referencí (a pokud jde o proměnnou na zásobníku, nelze ji mimo kontext použít). Z tohoto pohledu jsou clangovské bloky univerzálnější. Chceme-li psát přenositelný kód, musíme mít toto omezení na paměti.

C#

C# podporuje lambda výrazy od třetí verze. Modifikovatelné proměnné fungují podobně jako v clangu, ale není nutné uvádět __block nebo nějaký podobný modifikátor. Deklarujeme-li například int n = 0, vytvoří se taková proměnná většinou na zásobníku. Pokud ale překladač zjistí, že se používá v nějakém lambda výrazu, automaticky vytvoří tuto proměnnou na haldě. Pokud se tedy výraz použije i mimo lokální kontext, proměnná stále existuje (na zásobníku by zanikla), a navíc se vyhneme kopírování ze zásobníku na haldu. Jistou nevýhodou může být, že všechny proměnné jsou uvnitř výrazu modifikovatelné.

Objective-C

Pro ObjC platí to samé, co pro clangovské blogy v jazyce C. Je ale třeba dodat, jak se zachází s objekty.

Bloky se chovají jako standardní objekty, tj. místo Block_copy a Block_release můžeme použít zprávy copy a release. Odpovědnosti za správu živostnosti objektu se zbavíme pomocí autorelease: [[blok copy] autorelease] (copy zkopíruje objekt ze zásobníku na haldu a autorelease ho za nás v budoucnu z haldy odstraní).

Při použítí ARC se o bloky na haldě nemusíme starat vůbec, překladačem generované direktivy to udělají za nás.

Poznámka k objektům s čítačem referencí

ObjC a WinRT mají u objektů čítač referencí. Ukazatel na takový objekt se sémanticky chová spíše jako handle v Javě či C#. Při použití s lambda výrazy to znamená, že zatímco neobjektové proměnné se kopírují, objektům se pouze zvýší čítač referencí (a opět se sníží při zániku bloku).

Pokud tedy v C++/CX (WinRT) napíšeme

Trida^ obj = ref new Trida;
[obj](){ obj->neco(); };

instanci třídy Trida se inkrementuje čítač, protože obj je uvedené v capture seznamu lambda výrazu. Pokud tomu chceme zabránit (například se chceme vyhnout cyklu referencí), použijeme [&obj], pak si ale samozřejmě musíme dát pozor na zánik použité instance, lokálně vytvořené objekt nepředaný jinam automaticky zanikne s lokálním kontextem (ref new sice vytváří objekty na haldě, ale handle po odstranění ze zásobníku sníží čítač referencí na nulu a zavolá se destruktor).

V Objective-C se chová čítač referencí u objektů podobně. Je třeba ale mít na paměti, že při použití ARC se čítač referencí aktualizuje i u proměnných deklarovaných pomocí __block, zatímco bez ARC ne. Pokud v ARC chceme zamezit vytvoření cyklu referencí, musíme místo __block použít __weak nebo __unsafe_unretained.

Úskalí míchání nativního a řízeného kódu

zboj, 5. 12. 2011, 17:01 v kategorii Programovací jazyky,

Když píšete v nějakém jazyce s bajtkódem a GC (např. pro .NET nebo v Javě), narazíte občas na potřebu použít nativní kód (například kvůli výkonu nebo máte nějaký z dřívějška a nechce se vám ho přepisovat). Volání nativního kódu z řízeného a naopak s sebou přináší pár problémů, na které je třeba dát pozor.

Paměť

Voláme-li nativní kód z řízeného, obcházíme samozřejmě GC a musíme se o správu paměti postarat sami. V C++ sice (dovolím si parafrázovat jeden známý výrok) chytrý programátor new nepoužívá a blbci je to jedno, jenže v tomto případě se mu nevyhneme. Řízená třída (na řízené haldě) může narozdíl od nativních tříd obsahovat pouze ukazatel na instanci nativní třídy, tedy např. Test* inst, ale ne Test inst. Musíme tedy použít new Test s tím, že instanci zrušíme při likvidaci rodičovského objektu. Jenže o ten se stará GC, takže nad životním cyklem objektu nemáme kontrolu. Abychom zamezili úniku paměti, dáme nezbytné delete inst do finalizéru. Jenže nikdo nám negarantuje, že ten se vůbec kdy zavolá. Jediný stoprocentně funkční způsob je uvolnit objekt ve finalizéru a zároveň v destruktoru (ten u řízené třídy odpovídá rozhraní IDisposable). Musíme tedy mít:

~Trida() { delete inst; }
!Trida() { delete inst; }

Běhové prostředí se postará, že se zavolá právě jedna z těchto metod (tj. nikdo se delete inst neprovede dvakrát). Lepší je samozřejmě použít deterministickou destrukci (v C++ zásobníkovou sémantiku, v C# blok uvozený using), jinak bychom naprosto zbytečně okupovali paměť na neřízené haldě až do spuštění GC.

Chceme-li mít instanci řízené třídy v nativním objektu, narazíme na více potenciálních problémů. Přímo to není vůbec možné, ale máme k dispozici pomocné nativní třídy gcroot a auto_gcroot, do kterých můžeme řízený objekt "zabalit". Použití gcroot je důležité proto, aby GC věděl, že někde existuje reference na řízený objekt a nezrušil jej předčasně. Neméně důležitým důvodem je, že GC objekty v paměti přesouvá, pokud bychom měli jen holý okazatel, po jednom cyklu GC by se mohlo stát, že by ukazoval na naprosto náhodná data (pořád na stejnou adresu, ale objekt by už byl jinde).

Problém s přesunem řízených objektů v rámci haldy lze řešit také "přišpendlením" (z anglického pin). K dispozici máme pomocnou třídu pin_ptr, které dáme ukazatel a tím zabráníme přesunu objektu, pokud se během používání objektu spustí GC. Například pin_ptr<int> p = &a[0] přišpendlí celé řízené pole, dokud instance pin_ptr nezanikne. Je samozřejmé, že bychom tuto pomocnou třídu měli používat s rozumem, protože může způsobit fragmentaci řízené haldy (mnohdy je lepší použít podobnou třídu interior_ptr, která nezabraňuje přesunu objektu, ale automaticky zabalený ukazatel aktualizuje na novou adresu v paměti).

Použití gcroot s sebou nese jistou režii, máme-li tedy například gcroot<T^> wrapper, je lepší místo volání wrapper->Metoda() (operátor -> je přetížený podobně jako u chytrých ukazatelů) použít přetížený operátor přetypování (pokud chceme volat více metod nebo jednu metodu několikrát):

T^ obj = wrapper; obj->Metoda();

Pro úplnost dodávám, že výše uvedené není jen záležitostí Microsoftu, například Apple měl ve svém "Java bridge" blahé paměti (byl v OS X až do verze 10.5) něco velmi podobného - bylo možné míchat kód v ObjC a v Javě, přičemž problémy okolo GC se řešily velmi podobně. Javu už v Applu hodili přes palubu, ale stále je možné v rámci jednoho souboru mísit ObjC s C++ (ObjC sice není řízený jazyk ve smyslu .NETu, ale stále je možnost používat GC apod.), kde jsou některé problémy s kompatibilitou velmi podobné.

NB: V novém WinRT vše funguje ještě úplně jinak, z řízeného kódu lze používat jen nativní komponenty (ref class v C++/CX) a wrapper z .NETu manipuluje s čítačem referencí (destruktor či finalizér objekt uvolní automaticky). Při použití .NET komponenty z C++ naopak zanikne reference na řízený objekt, jakmile se u nativního wrapperu sníži čítač referencí na nulu (a řízený objekt se tak nechá na pospas GC).

Výjimky

Ošetření výjimek není zdaleka tak ošemetné. Pokud nativní kód vyhodí výjimku, vrstva mezi nativním a řízeným kódem (interop) výjimku odchytí a převede na řízenou, typicky SEHException (SEH=structured exception handling). Lepší je nativní výjimku odchytit v kódu a vyhodit (angl. rethrow) řízenou, například takto:

catch (std::exception& ex) {
throw gcnew Exception(gcnew String(ex.what()));
}

V případě problémů tak dostaneme přesnější informace, co se vlastně stalo.

CLR umožňuje vyhodit jako výjimku libovolný objekt (v C# nám překladač neumožní vyhodit cokoliv, co nedědí ze System.Exception, to je ale umělé omezení). Při použití neznámé knihovny je proto vždy lepší (v C#) odchytávat všechny objekty (nespecifikujeme Exception v bloku catch).

Pro porovnání, v ObjC++ nejsou výjimky z ObjC a C++ kompatibilní, musíme tedy jednu výjimku zachytit a vyhodit druhou. V ObjC se používají syntakticky odlišná klíčová slova - @try/@catch/@finally.