Nazwane parametry sprawiają, że kod jest łatwiejszy do odczytania, trudniejszy do napisania
Kiedy czytam fragment kodu, nazwane parametry mogą wprowadzić kontekst, który ułatwia zrozumienie kodu. Rozważmy na przykład tego konstruktora: Color(1, 102, 205, 170)
. Co to do diabła znaczy? Rzeczywiście, Color(alpha: 1, red: 102, green: 205, blue: 170)
byłoby o wiele łatwiejsze do odczytania. Ale niestety, Kompilator mówi „nie” - chce Color(a: 1, r: 102, g: 205, b: 170)
. Pisząc kod przy użyciu nazwanych parametrów, spędzasz niepotrzebnie dużo czasu na wyszukiwaniu dokładnych nazw - łatwiej jest zapomnieć dokładne nazwy niektórych parametrów, niż zapomnieć o ich kolejności.
To raz mnie ugryzło, gdy korzystałem z DateTime
interfejsu API, który miał dwie klasy rodzeństwa dla punktów i czasów trwania z prawie identycznymi interfejsami. Chociaż DateTime->new(...)
zaakceptował second => 30
argument, DateTime::Duration->new(...)
poszukiwany seconds => 30
i podobny dla innych jednostek. Tak, to absolutnie ma sens, ale pokazało mi to, że nazwane parametry - łatwość użycia.
Złe nazwy nawet nie ułatwiają czytania
Innym przykładem tego, jak nazwane parametry mogą być złe, jest prawdopodobnie język R. Ten fragment kodu tworzy wykres danych:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
Widać dwa argumenty pozycyjne dla x i y wierszy danych, a następnie na liście wymienionych parametrów. Istnieje wiele innych opcji z wartościami domyślnymi i tylko te są wymienione, których wartości domyślne chciałem zmienić lub wyraźnie określić. Kiedy zignorujemy to, że ten kod używa magicznych liczb i może skorzystać z wyliczeń (jeśli R miał jakiś!), Problem polega na tym, że wiele z tych parametrów jest raczej nieczytelnych.
pch
jest w rzeczywistości znakiem wykresu, glifem, który zostanie narysowany dla każdego punktu danych. 17
jest pustym okręgiem lub coś takiego.
lty
jest typem linii. Oto 1
linia ciągła.
bty
jest typem pudełka. Ustawienie tak, aby "n"
uniknąć rysowania ramki wokół wykresu.
ann
kontroluje wygląd adnotacji osi.
Dla kogoś, kto nie wie, co oznacza każdy skrót, opcje te są raczej mylące. Ujawnia to również, dlaczego R używa tych etykiet: Nie jako samodokumentujący się kod, ale (będąc językiem o dynamicznym typowaniu) jako klucze do mapowania wartości na ich poprawne zmienne.
Właściwości parametrów i podpisów
Podpisy funkcji mogą mieć następujące właściwości:
- Argumenty można zamówić lub nieuporządkowane,
- nazwany lub nienazwany,
- wymagane lub opcjonalne.
- Podpisy mogą być również przeciążone według rozmiaru lub typu,
- i może mieć nieokreślony rozmiar z varargs.
Różne języki lądują na różnych współrzędnych tego systemu. W C argumenty są uporządkowane, nienazwane, zawsze wymagane i mogą być varargs. W Javie sytuacja jest podobna, tyle że podpisy mogą być przeciążone. W celu C podpisy są uporządkowane, nazwane, wymagane i nie mogą być przeciążone, ponieważ jest to tylko cukier składniowy w okolicach C.
Dynamicznie wpisywane języki z varargs (interfejsy wiersza poleceń, Perl,…) mogą emulować opcjonalne nazwane parametry. Języki z przeciążeniem rozmiaru sygnatury mają coś w rodzaju opcjonalnych parametrów pozycyjnych.
Jak nie implementować nazwanych parametrów
Myśląc o nazwanych parametrach, zwykle zakładamy nazwane, opcjonalne, nieuporządkowane parametry. Ich wdrożenie jest trudne.
Parametry opcjonalne mogą mieć wartości domyślne. Muszą być określone przez wywoływaną funkcję i nie powinny być wkompilowane w kod wywołujący. W przeciwnym razie wartości domyślne nie mogą zostać zaktualizowane bez ponownej kompilacji całego zależnego kodu.
Teraz ważnym pytaniem jest, w jaki sposób argumenty są przekazywane do funkcji. Przy uporządkowanych parametrach argumenty można przekazywać do rejestru lub w odpowiedniej kolejności na stosie. Kiedy chwilowo wykluczamy rejestry, problem polega na tym, jak umieścić na stosie nieuporządkowane argumenty opcjonalne.
W tym celu potrzebujemy trochę porządku w stosunku do opcjonalnych argumentów. Co jeśli kod deklaracji zostanie zmieniony? Ponieważ kolejność jest nieistotna, zmiana kolejności w deklaracji funkcji nie powinna zmieniać położenia wartości na stosie. Należy również rozważyć, czy możliwe jest dodanie nowego opcjonalnego parametru. Z perspektywy użytkownika wydaje się, że tak, ponieważ kod, który wcześniej nie używał tego parametru, powinien nadal działać z nowym parametrem. Wyklucza to więc porządkowanie, takie jak użycie kolejności w deklaracji lub użycie kolejności alfabetycznej.
Rozważ to również w świetle podtypu i zasady podstawienia Liskowa - w skompilowanym wyjściu te same instrukcje powinny móc wywoływać metodę na podtypie z możliwie nowymi nazwanymi parametrami i na nadtypie.
Możliwe implementacje
Jeśli nie możemy mieć ostatecznego porządku, potrzebujemy trochę nieuporządkowanej struktury danych.
Najprostszą implementacją jest po prostu przekazanie nazwy parametrów wraz z wartościami. W ten sposób nazwane parametry są emulowane w Perlu lub za pomocą narzędzi wiersza poleceń. To rozwiązuje wszystkie problemy z rozszerzeniami wspomniane powyżej, ale może być ogromnym marnotrawstwem miejsca - nie jest opcją w kodzie krytycznym dla wydajności. Ponadto przetwarzanie tych nazwanych parametrów jest teraz o wiele bardziej skomplikowane niż po prostu usuwanie wartości ze stosu.
W rzeczywistości wymagania dotyczące miejsca można zmniejszyć, używając puli ciągów, co może zredukować późniejsze porównania ciągów do porównań wskaźników (z wyjątkiem sytuacji, gdy nie można zagwarantować, że ciągi statyczne są rzeczywiście połączone, w którym to przypadku oba ciągi będą musiały zostać porównane w Szczegół).
Zamiast tego moglibyśmy również przekazać sprytną strukturę danych, która działa jak słownik nazwanych argumentów. Jest to tanie po stronie dzwoniącego, ponieważ zestaw kluczy jest statycznie znany w miejscu połączenia. Pozwoliłoby to stworzyć idealną funkcję skrótu lub wstępnie obliczyć próbę. Odbiorca nadal będzie musiał sprawdzić, czy istnieją wszystkie możliwe nazwy parametrów, co jest nieco drogie. Coś takiego używa Python.
W większości przypadków jest to po prostu zbyt drogie
Jeśli funkcja o nazwanych parametrach ma być odpowiednio rozszerzalna, nie można przyjąć ostatecznego uporządkowania. Istnieją więc tylko dwa rozwiązania:
- Ustaw kolejność nazwanych parametrów jako część podpisu i nie zezwalaj na późniejsze zmiany. Jest to przydatne w przypadku kodu samodokumentującego, ale nie pomaga w przypadku opcjonalnych argumentów.
- Przekaż strukturę danych klucz-wartość do odbiorcy, który następnie musi wyodrębnić przydatne informacje. Jest to bardzo drogie w porównaniu i zwykle występuje tylko w językach skryptowych bez nacisku na wydajność.
Inne pułapki
Nazwy zmiennych w deklaracji funkcji mają zwykle jakieś wewnętrzne znaczenie i nie są częścią interfejsu - nawet jeśli wiele narzędzi dokumentacji nadal je pokaże. W wielu przypadkach potrzebujesz różnych nazw dla zmiennej wewnętrznej i odpowiedniego nazwanego argumentu. Języki, które nie pozwalają na wybranie widocznych z zewnątrz nazw nazwanych parametrów, nie zyskują wiele z nich, jeśli nazwa zmiennej nie jest używana w kontekście kontekstu wywoływania.
Problemem z emulacjami nazwanych argumentów jest brak statycznego sprawdzania po stronie wywołującej. Jest to szczególnie łatwe do zapomnienia, gdy przekazujesz słownik argumentów (patrząc na ciebie, Python). Jest to ważne, ponieważ przechodząc słownika jest częstym obejście, na przykład w JavaScript: foo({bar: "baz", qux: 42})
. W tym przypadku ani typy wartości, ani istnienie lub brak niektórych nazw nie mogą być sprawdzane statycznie.
Emulowanie nazwanych parametrów (w językach o typie statycznym)
Po prostu użycie ciągów jako kluczy i dowolnego obiektu jako wartości nie jest zbyt przydatne w obecności sprawdzania typu statycznego. Nazwane argumenty można jednak emulować za pomocą struktur lub literałów obiektowych:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});