Będąc w zgodzie z podzielanym przez siebie poglądem o kluczowej a często niedocenianej roli “małych” algorytmów, dzisiaj wezmę pod lupę funkcję do łączenia napisów, znaną większości jako join
. Ta przydatna operacja występuje w wielu językach i bibliotekach, a jej brak w pozostałych jest zwykle wyraźnie odczuwalny (tak, Java, o tobie mówię). Dobrze użyty join
– zwłaszcza w połączeniu z pewnymi specyficznymi mechanizmami językowi – potrafi zapobiec pisaniu niepotrzebnych pętli, znacząco redukując code bloat.
Ale po kolei. join
to operacja polegająca na złączeniu kolekcji napisów w jeden, odpowiednio sklejony łańcuch. Łączenie polega na tym, że w wyniku pomiędzy elementami kolekcji wstawiony jest pewien określony ciąg (“klej”). Najlepiej widać to na przykładzie:
Łatwo zauważyć, że join
jest w gruncie rzeczy przeciwieństwem funkcji split
, którą nieprzypadkowo kiedyś już opisywałem :)
W czym przejawia się przydatność tej operacji? Przede wszystkim rozwiązuje ona “problem ostatniego przecinka” przy wypisywaniu list. Tradycyjnie obchodzi się go mniej więcej tak:
for (int i = 0; i < (int)strings.length(); ++i) {
std::cout << strings[i];
if (i + 1 < (int)strings.length()) std::cout << ", ";
}[/cpp]
Instrukcja if
w tej pętli nie jest oczywiście szczytem elegancji. Gdybyśmy mieli tu funkcję join
wszystko byłoby o wiele czytelniejsze:
std::cout << join(", ", strings);[/cpp]
Drugą zaletą join
a jest jego dobra współpraca z modnymi ostatnio, funkcyjnymi rozszerzeniami wielu języków, pozwalająca w zwięzły sposób przetwarzać kolekcje obiektów. Jeśli na przykład mamy słownik (tudzież mapę/hash), to zamiana go na tekstowy odpowiednik klucz=wartość jest prosta:
Oczywiście jest tak wówczas, gdy na widok słowa kluczowego lambda
nie uciekamy z krzykiem ;-)
Na koniec tej krótkiej pogadanki wypadałoby jeszcze zaprezentować przykładową implementację omawianej funkcji. Ponieważ – jak napomknąłem wcześniej – doskwierał mi ostatnio jej brak w Javie, więc kod będzie w tym właśnie języku:
Z dokładnością do szczegółów generycznych kolekcji i operacji na stringach, powyższą implementację powinno się dać łatwo przetłumaczyć także na C++.
Osobiście uważam że to jednak lambda i map są bohaterami tego wpisu ;)
Dzięki nim join/split (mógłbyś też dać przykład z filter()) czy używanie *args jest tak przyjemne (choć potrafi to zaciemnić kod jeśli ktoś nie ma wprawy w czytaniu lambd).
Przykład z map
można było też zapisać bez tej funkcji:
Nie mogłem się jednak zdecydować, który z tych dwóch wariantów dla osób niezbyt biegłych w Pythonie będzie mniej niezrozumiały ;) Skłoniłem się ku map
, bo list comprehension nie jest tak oczywiście funkcyjnym ficzerem :)
Na wielkiego jeża, mogłeś chociaż przenieść tego ifa przed drukowanie elementu i zapisać go po prostu jako if (i)
:)
Notabene C++ ma coś takiego jak join, oczywiście na miksie szablonów, iteratorów i strumieni:
http://www.sgi.com/tech/stl/ostream_iterator.html
Brr. Idę umyć ręce :)
Istnieje ciekawsze rozwiązanie join bez ifa za każdym razem.
Jeżeli przeszkadza Ci narzut O(n) przy uruchamianiu length, to możesz to rozłożyć w pętli.
em…
bardziej pythonicznie by było (bez lambdy i mapy):
import os
data = {"fullscreen": 1, "width": 800, "height": 600}
print os.linesep.join(["%s=%s" % (key, val) for key, val in data.items()])
A ja od jakiegoś czasu stosuję takie, wg. mnie dość eleganckie rozwiązanie, które napotkałem podczas przeglądania znalezionego kodu opensourceowego. Szybkie, sprawne, zamiast if-a jest przypisanie literału, więc chyba efektywnie szybsze. Bez niepotrzebnego iteratora i sprawdzania długości: