It would be quite far-fetched to call JavaScript a functional language, for it lacks many more sophisticated features from the FP paradigm – like tail recursion or automatic currying. This puts it on par with many similar languages which incorporate just enough of FP to make it useful but not as much as to blur their fundamental, imperative nature (and confuse programmers in the process). C++, Python or Ruby are a few examples, and on the surface JavaScript seems to place itself in the same region as well.
Except that it doesn’t. The numerous different purposes that JavaScript code uses functions makes it very distinct, even though the functions themselves are of very typical sort, found in almost all imperative languages. Learning to recognize those different roles and the real meaning of function
keyword is essential to becoming an effective JS coder.
So, let’s look into them one by one and see what the function
might really mean.
If you’ve seen few good JavaScript libraries, you have surely stumbled upon the following idiom:
Any and all code is enclosed within an anonymous function
. It’s not even stored in a var
iable; it’s just called immediately so its content is just executed, now.
This round-trip may easily be thought as if doing absolutely nothing but there is an important reason for keeping it that way. The point is that JavaScript has just one global object (window
in case of web browsers) which is a fragile namespace, easily polluted by defining things directly at the script level.
We can prevent that by using “bracketing” technique presented above, and putting everything inside this big, anonymous function. It works because JavaScript has function scope and it’s the only type of non-global scope available to the programmer.
So in the example above, the function
is used to confine script’s code and all the symbols it defines. But sometimes we obviously want to let some things through, while restricting access to some others – a concept known as encapsulation and exposing an interface.
Perhaps unsurprisingly, in JavaScript this is also done with the help of a function
:
What we get here is normal JS object but it should be thought of more like a module. It offers some public interface in the form of increment
and getValue
functions. But underneath, it also has some internal data stored within a closure: the value
variable. If you know few things about C or C++, you can easily see parallels with header files (.h, .hpp, …) which store declarations that are only implemented in the code files (.c, .cpp).
Or, alternatively, you may draw analogies to C# or Java with their public and private (or protected) members of a class. Incidentally, this leads us to another point…
Let’s assume that the counter
object from the example above is practical enough to be useful in more than one place (a tall order, I know). The DRY principle of course prohibits blatant duplication of code such as this, so we’d like to make the piece more reusable.
Here’s how we typically tackle this problem – still using only vanilla function
s:
Pretty straightforward, right? Instead of calling the function on a spot, we keep it around and use to create multiple objects. Hence the function becomes a constructor for them, while the whole mechanism is nothing else but a foundation for object-oriented programming.
We have now covered most (if not all) roles that functions play when it comes to structuring JavaScript code. What remains is to recognize how they interplay with each other to control the execution path of a program. Given the highly asynchronous nature of JavaScript (on both client and server side), it’s totally expected that we will see a lot of functions in any typical JS code.
Przeglądając plik źródłowy programu w dowolnym niemal języku, gdzieś bardzo blisko początku znajdziemy zawsze region z importami. Niekoniecznie będą one oznaczone słowem kluczowym import
– czasem to będzie using
, być może do spółki z #include
– ale zawsze będą robiły zasadniczo to samo. Chodzi o poinformowanie kompilatora lub interpretera, że w tym pliku z kodem używamy takich-a-takich funkcji/klas/itp. z takich-a-takich modułów/pakietów. Dzięki temu “obce” nazwy użyte w dalszej części będą mogły być połączone z symbolami zdefiniowanymi gdzie indziej.
Każdy import w jakiś sposób rozszerza więc przestrzeń nazw danego modułu i zazwyczaj wszystko jest w porządku, dopóki dokładnie wiemy, jak to robi. Dlatego też powszechnie niezalecane są “dzikie” importy (wild imports), które nie wyliczają jawnie wszystkich dodawanych nazw, zwykle ukrywając je za gwiazdką (*
). Ale nawet jeśli ich nie używamy, to nie oznacza to, że żadne problemy z importowanymi nazwami nas nie spotkają. Oto kilka innych potencjalnych źródeł kłopotów:
import foo.bar.baz;
wprowadza do przestrzeni modułu nazwę baz
(czyli niekwalifikowaną) w przypadku Javy. W przypadku Pythona ten sam efekt wymaga z kolei instrukcji from foo.bar import baz
, a zwykła instrukcja import
da nam jedynie kwalifikowaną nazwę foo.bar.baz
– która z kolei w Javie i C# jest dostępna bez żadnych importów, a w C++ po dodaniu dyrektywy #include
… Całkiem intuicyjne, czyż nie? ;-) Skoro tak, to dodajmy do tego jeszcze fakt, iż…Podsumowując, importy – chociaż często zarządzane prawie całkowicie przez IDE – to w sumie dość poważna sprawa i warto zwrócić na nie uwagę przynajmniej od czasu do czasu.
Jeśli w C++ piszemy jakąś bibliotekę (albo nawet ogólniej: kod wielorazowego użytku), to zgodnie z dobrymi praktykami powinniśmy jej symbole zamknąć przynajmniej w jedną osobną przestrzeń nazw (namespace). Dzięki temu zapobiegniemy zminimalizujemy możliwość kolizji identyfikatorów w kodzie, który z naszego dzieła będzie korzystał – a także potencjalnie z innych bibliotek.
Nie wszystkie z nich jednak mogą być tak ładnie napisane – choćby dlatego, że któraś może być przeznaczona oryginalnie dla języka C. Najbardziej typowy przykład? Windows API. Dołączenie windows.h zasypuje globalną przestrzeń nazw istną lawiną symboli odpowiadających tysiącom funkcji czy typów zadeklarowanych w tym nagłówku. Nie jest to specjalnie dobre.
Jak temu zaradzić? Bardzo prostą, ale nierozwiązującą wszystkich problemów metodą jest stworzenie własnego nagłówka “opakowującego” ten biblioteczny w nową przestrzeń nazw:
Założenie jest takie, żeby wynikowego pliku nagłówkowego (foo.h) używać następnie w miejsce oryginalnego (foobar.h). Wtedy wszystkie symbole w nim zadeklarowane znajdą się wewnątrz nowej przestrzeni nazw, foo
.
Wszystkie?… Nie! Pakując kod napisany w stylu C bezpośrednio do przestrzeni nazw nie osiągniemy bowiem wszystkich celów, którym namespace‘y przyświecają. Owszem, da się co nieco poprawić: jeśli np. wspomniany windows.h zamknęlibyśmy w przestrzeni win
, to poniższy kod będzie jak najbardziej działał:
podczas gdy wersja bez przedrostków win::
już niezupełnie. Jednak nie jest to całkowity – nomen omen – win, bo z kolei takie wywołanie:
skutkuje już niestety failem :) Nasza przestrzeń nie może bowiem zamknąć wszystkiego, gdyż nie podlegają jej dyrektywy preprocesora – a w szczególności #define
. Pech polega na tym, że właśnie #define
jest w C podstawowym sposobem definiowania stałych, więc użyta wyżej nazwa SW_MINIMIZE
jest (w windows.h) określona po prostu jako:
Próba jej kwalifikowania powoduje zatem powstanie nieprawidłowego ciągu win::6
i słuszne narzekania kompilatora.
Nasz pojemnik (na nazwy) jest więc dziurawy i niestety nic z tym nie da się zrobić. Tak to już jest, gdy wciąż trzeba mieć do czynienia z API, które w tym przypadku liczy sobie – bagatelka – ponad 20 lat!
W C++ istnieje słowo kluczowe using
, z którym zetknął się chyba każdy. Zwykle dotyczy to nieśmiertelnej linijki:
Dlatego też słowo to kojarzy się przede wszystkim z przestrzeniami nazw (namespaces), a najczęściej tylko i wyłącznie z nimi.
Jednak using
ma też swoje zastosowanie – i to zapewne znacznie ciekawsze – przy definiowaniu klas. Formalnie rzecz ujmując, słówko to pozwala wprowadzić składową pochodzącą z klasy bazowej (metodę lub pole) do zasięgu klasy pochodnej. Niezbyt to ekscytujące na pierwszy rzut oka, ale faktem jest, że dzięki skorzystaniu z using
możemy dokonać przynajmniej jednej koniecznej czasami operacji. Możliwa jest mianowicie zmiana kontroli dostępu do danego składnika klasy, czyli określenie, czy ma on być prywatny czy może publiczny.
Wyobraźmy sobie na przykład, że w klasie bazowej mamy jakieś funkcje niepubliczne, które chcemy udostępnić na zewnątrz w klasie pochodnej:
Sytuacja nie jest wcale taka hipotetyczna. Sam miałem ostatnio taki przypadek, gdy najwygodniej było po prostu wbudować w klasę bazową pewną ukrytą funkcjonalność, która była używana przez wiele z jej klas pochodnych do ich wewnętrznych celów. Jednak niektóre z tych klas musiały tę “bazową” funkcjonalność udostępnić na zewnątrz jako publiczną. I tu przydało się użycie using
w sposób zaprezentowany wyżej. (Dla zainteresowanych wspomnę, że opisywana sytuacja praktycznie wystąpiła u mnie w kodzie systemu GUI :]).
Niestety, przestrzenie nazw w C++ nie mają takiego wdzięku jak pakiety w Javie czy assemblies w .NET. Zwłaszcza w połączeniu z plikami nagłówkowymi potrafią one sprawić nieco problemów. Jeśli bowiem nie będziemy uważali, to możemy dość łatwo zniwelować korzyści, jakie płyną z ich używania.
Ale po kolei. Jeśli mamy do czynienia z modułem kodu (plikiem .cpp), to możemy bez żadnych skrupułów umieszczać w nim deklaracje using
lub using namespace
. Jeśli tylko nie prowadzi to do niejednoznaczności, wszystko jest w porządku, bowiem zasięg tych deklaracji ogranicza się tylko i wyłącznie do tego jednego pliku.
Gorzej jest niestety z plikami nagłówkowymi. Ponieważ włączane są one tu i ówdzie przy pomocy dyrektywy #include
, nie można tak po prostu wpisywać w nich deklaracji using namespace
. Zostałyby one bowiem rozpropagowane do wszystkich plików, które dany nagłówek włączają, efektywnie niwelując wszelkie korzyści zamknięcia fragmentów kodu w przestrzeń nazw. Bo co to za namespace, który wszyscy “rozpakowują” i przenoszą jego zawartość do przestrzeni globalnej?…
Dlatego nie ma rady: w plikach nagłówkowych można używać wyłącznie nazw kwalifikowanych – poprzedzonych wszystkimi nazwami przestrzeni, jak np. XC::Base::GC::BlockInfo
. W przeciwnym wypadku na pewno zaśmiecimy sobie którąś z przestrzeni (najczęściej globalną) identyfikatorami, które do niej nie należą – co będzie widoczne w systemach podpowiadania takich jak IntelliSense.
W czasach panowania języków proceduralnych i strukturalnych – jak C czy Pascal – możliwości ukrywania i ochrony kodu przed niepowołaną zmianą były, delikatnie mówiąc, słabe. Wszystkie funkcje, pola i struktury były dostępne na zewnątrz i tylko dzięki programistycznej kurtuazji (i czytaniu dokumentacji technicznej, o ile istniała) zawdzięczało się poprawne działanie kodu.
Sytuacja poprawiła się wraz z językami obiektowymi, takimi jak C++. Mamy już klasy, których składniki mogą być określone jako publiczne, chronione (protected
) albo prywatne. Jeżeli tylko odpowiednio oznaczymy składowe należące do implementacji, nikt niepowołany nie będzie miał do nich dostępu zwyczajnymi środkami.
Od tego czasu świat poszedł jednak do przodu pod względem technik organizacji kodu. Nowsze języki programowania – jak Java, C# czy Python – posiadają wbudowane mechanizmy pakietów (zwanych w .NET złożeniami – assemblies). Dzięki nim można grupować klasy wykonujące wspólne zadania i zapewniać dostęp do wybranych składowych wszystkich klasom z danego pakietu. W precyzyjnym określaniu widoczności celuje zwłaszcza C#.
W C++ teoretycznie mamy dość podobne możliwości, oferowane przez kilka osobnych mechanizmów językowych. Czymś co zdaje się najbardziej przypominać pakiety lub złożenia są przestrzenie nazw (namespaces). Ich przeznaczeniem jest przede wszystkich jednak zabezpieczenie przed dublowaniem i niejednoznacznością nazw. Na przykład klasa string
z STL jest zawarta w przestrzeni std
i dzięki temu nie będzie pomylona z jakąś inną klasą łańcuchów znakowych o tej samej nazwie.
Jednak przestrzenie nazw to bardzo ułomny sposób pakietowania. Elementy jednej przestrzeni (klasy, funkcje) domyślnie są dla siebie tak samo obce, jak te należące do różnych przestrzeni. Można to oczywiście zmienić, korzystając z deklaracji przyjaźni; wymaga to jednak znajomości wszystkich tych “przyjaciół” – czyli odpowiednich deklaracji zapowiadających – co jest bardzo niewygodne. Naturalnie można powiedzieć, że wtedy przynajmniej wiemy, kto nas “podgląda”. Lecz co z tego, skoro w takim zaprzyjaźnianiu stosować można wyłącznie zasadę wszystko albo nic: klasa/funkcja będzie widziała albo wszystko składowe, albo tylko publiczne (domyślnie). Bez pokrętnych sztuczek nie da się udostępnić pewnych składników tylko w ramach “pakietu”.
Całkiem osobną kwestią jest też to, jak mechanizm przestrzeni nazw współpracuje z tą zakałą C++, czyli plikami nagłówkowymi i dyrektywą #include
. W nowszym językach wystarcza instrukcja podobnej do tej:
żeby jednocześnie zadeklarować użycie pakietu i uchronić się przed koniecznością używania kwalifikowanych nazw (w tym przypadku np. ListBox
zamiast System.Windows.Forms.ListBox
). W C++ potrzebujemy do tego dwóch dyrektyw:
i niby wszystko byłoby w porządku. Problem w tym, że wielce inteligentny mechanizm plików nagłówkowych oraz #include
sprawia, że nazw niekwalifikowanych możemy używać praktycznie tylko w plikach *.cpp. Gdyby bowiem umieścić using namespace
w nagłówku, to instrukcja ta zostałaby bezmyślnie powielona we wszystkich plikach, które ten nagłówek dołączają. A wtedy przestrzeń nazw nie spełniałaby nawet tej najprymitywniejszej funkcji, czyli ochrony przed wieloznacznością powtarzających się nazw.
Tak naprawdę to same przestrzenie nazw nie są złe. Nawet same pliki nagłówkowe z C++ nie byłyby aż tak bardzo złe, gdyby ich zadaniem nie było tylko ułatwianie życia kompilatorowi. Dopiero połączenie tych dwóch rzeczy sprawia, że zdecydowanie odechciewa się używać którejkolwiek z nich ;P
Istnieje jednak pewien optymistyczny akcent. Jeśli kiedykolwiek zostanie zniesiony w C++ podział na dwa rodzaje plików z kodem, wówczas i przestrzenie nazw będą musiały się przekształcić w coś podobnego do pakietów Javy czy złożeń z .NET. Podobnie jest w drugą stronę: jeżeli C++ dorobi się mechanizmu pakietów, wtedy pliki nagłówkowe i dyrektywa #include
nie będą miały wielkiego sensu. Wypada więc tylko kibicować Komitetowi Standaryzacyjnemu w wykonaniu tego pierwszego kroku, bo drugi prawdopodobnie zrobią siłą rozpędu :)