Hlavní navigace

Lambda výrazy, uzávěry, bloky

25. 7. 2011 12:23 (aktualizováno) zboj

Lambda výrazy (v některých jazycích označované jako uzávěry nebo bloky) se v posledních letech rozšířily do většiny moderních programovacích jazyků. Tento přehledový článek se věnuje některým aspektům jejich implementace a použití.

Lambda výrazy jsou v mnohých jazycích literály, tj. jedná se o pevnou součást jazyka a lze je přiřazovat do proměnných, používat jako argumenty při volání funkcí či metod apod. Příkladem budiž zápis jednoduchého lambda výrazu v jazyce Scheme (dialekt Lispu):

(lambda (x) (+ x 1))

Tento výraz reprezentuje funkci s jedním parametrem, která vrací tento parametr zvýšený o jedničku. V mnohem populárnějším Javascriptu by byl tento výraz zapsán takto:

function(x) { return x + 1; }

Velice užitečnou vlastností lambda výrazu je, že ve většině jazyků mají přístup k proměnným mimo vlastní definici (proto se jim také říká uzávěry, anglicky closures, uzavírají totiž lexikální kontext funkce nebo metody, v níž jsou definovány). Například lambda výraz, který vrací následující funkce, má přístup k proměnné n i mimo funkci f.

function f() { var n = 1; return function() { return n++; }; }

Této vlastnosti lambda výrazů využívají mnohé knihovny v moderních programovacích jazycích ke zjednodušení kódu.

Smalltalk

Ve Smalltalku se použití lambda výrazů (nazývaných bloky) nelze vyhnout, používají se totiž všude. Např. místo konstrukce if-then-else se pošle objektu reprezentujícímu podmínku zpráva ifTrue: nebo ifTrue:ifFalse:, jejímiž argumenty jsou bloky, které se vykonají podle platnosti podmínky:

aBoolean ifTrue: [ ... ] ifFalse: [ ... ]

Podobně se ve Smalltalku řeší cykly (for, while) apod.

Java

Java lambda výrazy nemá (existuje jejich návrh a experimentální implementace, ta se ale nedostala do sedmé verze Javy a vývojáři tak musí počkat na její osmou verzi). Poskytuje ale vývojářům tzv. anonymní třídy, které se lambda výrazům v mnoha ohledech podobají. Příkladem budiž kód z GWT (Google Web Toolkit) pro asynchronní volání služeb na serveru:

service.doSomething(new AsyncCallback<String>() { public void onSuccess(String result) { ... } ... });

Tento poněkud „ukecaný“ zápis je asynchronní verzí funkce String doSomething(). Důvodem použití anonymní třídy je kontext nasazení GWT—webová aplikace v prohlížeči. Uvedený fragment kódu totiž volá kód na serveru (pomocí XMLHttpRequest), což může při pomalém připojení k internetu nějakou dobu trvat, takže uživatel může tak akorát pár sekund zírat na zamrzlý prohlížeč (javascripový kód se provádí synchronně v jednom vlákně) a přesýpací hodiny (na Macu na točící se duhovou kouli). Asynchronní volání běh javascript neblokuje, po zavolání funkce doSomething se kód provádí dál a výsledek se zpracuje až v okamžiku, kdy je odpověď ze serveru k dispozici.

I v Javě má kód anonymních metod přístup k lexikálnímu kontextu, ovšem bez omezení pouze k proměnným nadřazené třídy. Lokální proměnné jsou v kódu anonymních tříd přístupné pouze v případě, že jsou definovány jako final (teda jako neměnné). Toto omezení souvisí s implementací anonymních tříd.

C#

C# obsahuje lambda výrazy od verze 3.0. Výše uvedený výraz v jazyce Scheme by v C# vypadal takto:

x => x + 1

Jak je vidět, zápis v C# je méně „ukecaný“ než v Javascriptu, žádné další výhody ovšem nepřináší.

C++

Doposud jsme se zabývali jazyky s automatickou správou paměti (GC, z angl. garbage collection), které zaručují, že objekty (včetně lambda výrazů, resp. bloků) existují, dokud na ně existuje reference, a naásledně jsou automaticky zrušeny. Ovšem i v jazycích bez GC (např. C++ nebo Objective-C) lze lambda výrazy používat, vývojář ovšem musí dávat pozor na životní cyklus objektů.

V C++ zavádí lambda výrazy návrh C++0×. Výše uvedený příklad pro C# by vypadal takto (typem tohoto výrazu je  function<int(int)>):

[](int x) { return x + 1; }

S lokální proměnnými z okolního kontextu je to trošku složitější. V lambda výrazu použité proměnné je nutné deklarovat v hranatých závorkách (v předchozím příkladu jsou prázdné, protože kontext se nepoužívá). Pro každou deklarovanou proměnnou navíc vývojář musí určit, zda se používá její kopie, nebo reference. Proč tomu tak je si ukážeme na příkladu z Objective-C

Objective-C

Smalltalkem inspirované bloky byly v Objective-C od počátku, ale verze používaná firmou Next (kterou pak koupil Apple) je až donedávna ignorovala. Nová implementace se liší od návrhu C++0×, příklady z C# a C++ uvedené výše se v Objective-C zapíšou takto:

^(int x) { return x + 1; }

Implementace v Objective-C je navržena s ohledem na ruční správu paměti. Od verze OS X 10.5 (tedy už poměrně dlouho) má sice Objective-C automatickou správu paměti (GC), ta ale není k dispozici pod iOS. Lambda výrazy (v Objective-C nazývané bloky) jsou literály, které se vždy vytváří na zásobníku. To je sice rychlé, ale takový výraz nelze použít mimo funkci, v níž je definován. Proto poskytuje běhové prostředí (runtime) funkci Block_copy, která kopíruje bloky ze zásobníku na haldu. Toto řešení má tu nevýhodu, že takto zkopírovaný blok je nutné, když už není používán, uvolnit funkcí Block_release. (V objektově-orientovaném kódu se bloky chovají jako běžné objekty a lze jim posílat zprávy copy, release, případně  autorelease).

Lokální proměnné z okolního kontextu jsou standardně v kódu bloku neměnné. U atomických hodnot se používá jejich kopie (pokud později jejich hodnutu změníme, v bloku bude stále k dispozici původní hodnota), u objektů použitých v blocích běhové prostředí automaticky zvyšuje (a při zániku bloku snižuje) čítač referencí, protože takový objekt může být použit až mnohem později). Pokud chceme proměnné z okolního kontextu měnit, je nutné je deklarovat pomocí __block (např. __block int n). Pokud takto deklarujeme objekt, musíme se o jeho životní cyklus postarat sami, běhové prostředí v tomto případě čítač referencí neaktualizuje.

Praktický příklad

Praktické použití lambda výrazů si ukážeme na příkladu knihovní funkce, která setřídí pole objektů. Ve standardní knihovně pro Objective-C od počátku existuje metoda sortedArrayUsingSelector:, která vrací pole setříděné podle methody předané jako selector, tedy například:

NSArray* sortedArray = [anArray sortedArrayUsingSelector: @selector(compare:)];

Problémem při použití této metody je nutnost definice metody compare: v definici třídy, jejíž instance jsou prvky tříděného pole. Po zavedení bloků můžeme kód pro třídění uvést přímo jako argument metody:

NSArray* sortedArray = [anArray sortedArrayUsingComparator: ^(id obj1, id obj2) { return [obj1.name compare: obj2.name]; }]

Pokud Vám připadá Objective-C poněkud obskurní, podívejte se na dokumentaci metody sort třídy Array v Javascriptu, která se používá zcela stejně:

anArray.sort(function(obj1, obj2) { return obj1.age - obj2.age; });

Z výše uvedených příkladů je zřejmé, že lambda výrazy, resp. bloky jsou velmi užitečný a mocný nástroj, který při správném použití značně zjednodušuje kód aplikací a zejména návrh knihoven a jejich rozhraní.

Sdílet