Radzenie sobie z nieznajomością nazw parametrów funkcji podczas jej wywoływania

13

Oto problem dotyczący programowania / języka, o którym chciałbym usłyszeć twoje przemyślenia.

Opracowaliśmy konwencje, których większość programistów (powinna) przestrzegać, które nie są częścią składni języków, ale służą do zwiększenia czytelności kodu. Oczywiście są one zawsze przedmiotem dyskusji, ale są przynajmniej niektóre podstawowe koncepcje, które większość programistów uważa za przyjemne. Właściwe nazywanie zmiennych, nazywanie w ogólności, sprawianie, że twoje linie nie są skandalicznie długie, unikanie długich funkcji, enkapsulacji itp.

Jest jednak problem, na który nie znalazłem nikogo, kto komentuje i może to być największa z całej gamy. Problem polega na anonimowości argumentów podczas wywoływania funkcji.

Funkcje wynikają z matematyki, w której f (x) ma jasne znaczenie, ponieważ funkcja ma znacznie bardziej rygorystyczną definicję niż zwykle w programowaniu. Czyste funkcje w matematyce mogą zrobić znacznie mniej niż w programowaniu i są znacznie bardziej eleganckim narzędziem, zwykle przyjmują tylko jeden argument (który zwykle jest liczbą) i zawsze zwracają jedną wartość (również zwykle liczbę). Jeśli funkcja przyjmuje wiele argumentów, prawie zawsze są to tylko dodatkowe wymiary domeny funkcji. Innymi słowy, jeden argument nie jest ważniejszy od innych. Oczywiście są uporządkowane wyraźnie, ale poza tym nie mają uporządkowania semantycznego.

W programowaniu mamy jednak więcej funkcji definiujących swobodę, i w tym przypadku argumentowałbym, że to nie jest dobra rzecz. Często zdarza się, że zdefiniowano taką funkcję

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Patrząc na definicję, jeśli funkcja jest napisana poprawnie, jest całkowicie jasne, co jest. Podczas wywoływania funkcji możesz mieć nawet magię inteligencji / uzupełniania kodu w twoim edytorze IDE / edytorze, która powie ci, jaki powinien być następny argument. Ale poczekaj. Jeśli potrzebuję tego, kiedy piszę rozmowę, czy nie brakuje nam czegoś? Osoba czytająca kod nie ma zalet IDE i dopóki nie przejdzie do definicji, nie ma pojęcia, który z dwóch prostokątów przekazanych jako argumenty jest używany do czego.

Problem idzie nawet dalej. Jeśli nasze argumenty pochodzą z jakiejś zmiennej lokalnej, mogą wystąpić sytuacje, w których nie wiemy nawet, jaki jest drugi argument, ponieważ widzimy tylko nazwę zmiennej. Weźmy na przykład ten wiersz kodu

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Jest to złagodzone w różnych zakresach w różnych językach, ale nawet w językach ściśle typowanych, a nawet jeśli rozsądnie nazywasz swoje zmienne, nawet nie wspominasz o typie zmiennej, gdy przekazujesz ją do funkcji.

Jak to zwykle bywa z programowaniem, istnieje wiele potencjalnych rozwiązań tego problemu. Wiele z nich jest już zaimplementowanych w popularnych językach. Na przykład parametry o nazwie w języku C #. Jednak wszystko, co wiem, ma znaczące wady. Nazewnictwo każdego parametru w każdym wywołaniu funkcji nie może prowadzić do czytelnego kodu. Wydaje się, że może przerastamy możliwości, jakie daje nam programowanie tekstu. Odeszliśmy od tekstu JUST w prawie każdym obszarze, ale nadal kodujemy to samo. Potrzebujesz więcej informacji do wyświetlenia w kodzie? Dodaj więcej tekstu. W każdym razie robi się to trochę stycznie, więc zatrzymam się tutaj.

Jedna odpowiedź, którą dostałem do drugiego fragmentu kodu jest taka, że ​​prawdopodobnie najpierw rozpakujesz tablicę do niektórych nazwanych zmiennych, a następnie użyjesz tych, ale nazwa zmiennej może oznaczać wiele rzeczy, a sposób jej wywoływania niekoniecznie mówi ci, jak powinna być interpretowane w kontekście wywoływanej funkcji. W zakresie lokalnym możesz mieć dwa prostokąty o nazwach leftRectangle i rightRectangle, ponieważ to właśnie one reprezentują semantycznie, ale nie musi rozciągać się na to, co reprezentują, gdy podano je funkcji.

W rzeczywistości, jeśli twoje zmienne są nazwane w kontekście wywoływanej funkcji, to wprowadzasz mniej informacji, niż mogłoby to być potencjalnie w przypadku tego wywołania funkcji i na pewnym poziomie, jeśli prowadzi to do gorszego kodu. Jeśli masz procedurę, w wyniku której powstaje prostokąt przechowywany w rectForClipping, a następnie inną procedurę zapewniającą rectForDrawing, wówczas faktyczne wywołanie DrawRectangleClipped to tylko ceremonia. Linia, która nie znaczy nic nowego, i jest po to, aby komputer wiedział, czego dokładnie chcesz, mimo że już wyjaśniłeś to swoim nazewnictwem. To nie jest dobra rzecz.

Naprawdę chciałbym usłyszeć nowe perspektywy na ten temat. Jestem pewien, że nie jestem pierwszym, który uważa to za problem, więc jak to rozwiązać?

Darwin
źródło
2
Jestem zdezorientowany, na czym dokładnie polega problem ... wydaje się, że jest tu kilka pomysłów, nie jestem pewien, który z nich jest twoim głównym punktem.
FrustratedWithFormsDesigner
1
Dokumentacja funkcji powinna wskazywać, co robią argumenty. Możesz sprzeciwić się temu, że ktoś czytający kod może nie mieć dokumentacji, ale tak naprawdę nie wiesz, co robi kod i jakiekolwiek znaczenie, jakie wyciąga z czytania, jest wyuczonym zgadywaniem. W każdym kontekście, w którym czytelnik musi wiedzieć, że kod jest poprawny, będzie potrzebował dokumentacji.
Doval
3
@Darwin W programowaniu funkcjonalnym wszystkie funkcje mają wciąż tylko 1 argument. Jeśli musisz przekazać „wiele argumentów”, parametrem jest zwykle krotka (jeśli chcesz, aby zostały uporządkowane) lub rekord (jeśli nie chcesz, aby były). Ponadto tworzenie wyspecjalizowanych wersji funkcji w dowolnym momencie jest banalne, dzięki czemu można zmniejszyć liczbę potrzebnych argumentów. Ponieważ prawie każdy język funkcjonalny zapewnia składnię krotek i rekordów, łączenie wartości jest bezbolesne, a Ty otrzymujesz kompozycję za darmo (możesz łączyć funkcje zwracające krotki z tymi, które pobierają krotki.)
Doval
1
@Bergi Ludzie generalizują znacznie więcej w czystym FP, więc myślę, że same funkcje są zwykle mniejsze i liczniejsze. Mógłbym być jednak daleko. Nie mam dużego doświadczenia w pracy nad prawdziwymi projektami z Haskellem i gangiem.
Darwin
4
Myślę, że odpowiedź brzmi: „Nie nazywaj swoich zmiennych„ deserializedArray ””?
whatsisname

Odpowiedzi:

10

Zgadzam się, że sposób, w jaki funkcje są często używane, może być mylącą częścią pisania kodu, a zwłaszcza czytania kodu.

Odpowiedź na ten problem częściowo zależy od języka. Jak wspomniałeś, C # nazwał parametry. Rozwiązanie problemu C w rozwiązaniu C wymaga bardziej opisowych nazw metod. Na przykład stringByReplacingOccurrencesOfString:withString:jest metodą o wyraźnych parametrach.

W Groovy niektóre funkcje pobierają mapy, pozwalając na składnię podobną do następującej:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

Ogólnie można rozwiązać ten problem, ograniczając liczbę parametrów przekazywanych do funkcji. Myślę, że 2-3 to dobry limit. Jeśli wydaje się, że funkcja wymaga więcej parametrów, powoduje, że przemyślam projekt. Jednak odpowiedź na to pytanie może być trudniejsza. Czasami próbujesz zrobić za dużo w funkcji. Czasami warto rozważyć klasę do przechowywania parametrów. Ponadto w praktyce często stwierdzam, że funkcje, które przyjmują dużą liczbę parametrów, zwykle mają wiele z nich jako opcjonalnych.

Nawet w języku takim jak Objective-C sensowne jest ograniczenie liczby parametrów. Jednym z powodów jest to, że wiele parametrów jest opcjonalnych. Na przykład zobacz rangeOfString: i jego odmiany w NSString .

Wzorem, którego często używam w Javie, jest użycie klasy płynnego stylu jako parametru. Na przykład:

something.draw(new Box().withHeight(5).withWidth(20))

Wykorzystuje klasę jako parametr, a dzięki klasie płynnego stylu kod jest czytelny.

Powyższy fragment kodu Java pomaga również tam, gdzie kolejność parametrów może nie być tak oczywista. Zwykle zakładamy ze współrzędnymi, że X występuje przed Y. I zwykle postrzegam wysokość przed szerokością jako konwencję, ale wciąż nie jest to zbyt jasne ( something.draw(5, 20)).

Widziałem także niektóre funkcje, drawWithHeightAndWidth(5, 20)ale nawet te nie mogą przyjąć zbyt wielu parametrów, w przeciwnym razie stracisz czytelność.

David V.
źródło
2
Kolejność, jeśli będziesz kontynuować na przykładzie Java, może być bardzo trudna. Na przykład porównaj następujące konstruktory z awt: Dimension(int width, int height)i GridLayout(int rows, int cols)(liczba rzędów to wysokość, co oznacza GridLayout, że najpierw ma wysokość, a Dimensionszerokość).
Pierre Arlaud
1
Takie rozbieżności zostały również bardzo krytykowane z PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), na przykład: array_filter($input, $callback)kontra array_map($callback, $input), strpos($haystack, $needle)kontraarray_search($needle, $haystack)
Pierre Arlaud
12

Najczęściej rozwiązuje się to przez dobre nazywanie funkcji, parametrów i argumentów. Jednak już to zbadałeś i odkryłeś, że ma wady. Większość tych braków jest zmniejszana przez utrzymywanie funkcji na niskim poziomie, z niewielką liczbą parametrów, zarówno w kontekście wywoływanym, jak i wywoływanym. Twój konkretny przykład jest problematyczny, ponieważ funkcja, którą wywołujesz, próbuje wykonać kilka rzeczy naraz: określ prostokąt podstawowy, określ obszar obcinania, narysuj go i wypełnij określonym kolorem.

To trochę jak próba napisania zdania, używając tylko przymiotników. Umieść więcej czasowników (wywołań funkcji), stwórz temat (obiekt) dla swojego zdania, a łatwiej jest to przeczytać:

rect.clip(clipRect).fill(color)

Nawet jeśli clipRecti colormieć straszliwe nazwy (i nie powinien), nadal można dostrzec swoje typy z kontekstu.

Twój zdesrializowany przykład jest problematyczny, ponieważ kontekst wywołujący próbuje zrobić zbyt wiele naraz: deserializowanie i rysowanie czegoś. Musisz przypisać nazwy, które mają sens i wyraźnie rozdzielają dwa obowiązki. Co najmniej coś takiego:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Wiele problemów z czytelnością jest spowodowanych próbą zwięzłości, pomijaniem etapów pośrednich wymaganych przez ludzi do rozpoznania kontekstu i semantyki.

Karl Bielefeldt
źródło
1
Podoba mi się pomysł ciągnięcia wielu wywołań funkcji w celu wyjaśnienia znaczenia, ale czy to nie tylko taniec wokół problemu? Zasadniczo „Chcę napisać zdanie, ale język, który pozywam, nie pozwala mi, więc mogę użyć tylko najbliższego odpowiednika”
Darwin
@Darwin IMHO nie można tego poprawić, czyniąc język programowania bardziej naturalnym. Języki naturalne są bardzo niejednoznaczne i możemy je zrozumieć tylko w kontekście i nigdy nie jesteśmy pewni. Wywołania funkcji ciągów są znacznie lepsze, ponieważ każdy termin (najlepiej) ma dokumentację i dostępne źródła, a nawiasy i kropki wyjaśniają strukturę.
maaartinus
3

W praktyce rozwiązuje to lepszy projekt. Wyjątkowo rzadkie jest, aby dobrze napisane funkcje przyjmowały więcej niż 2 dane wejściowe, a kiedy to się zdarza, nie jest tak często, że wiele danych wejściowych nie może być agregowanych w jakiś spójny pakiet. Ułatwia to rozkładanie funkcji lub agregowanie parametrów, dzięki czemu funkcja nie robi zbyt wiele. Jedno ma dwa wejścia, staje się łatwe do nazwania i znacznie jaśniejsze na temat tego, które wejście jest które.

Mój język zabawek miał pojęcie wyrażeń, aby sobie z tym poradzić, a inne języki programowania bardziej naturalne skupiały się na tym, ale wszystkie mają inne wady. Co więcej, nawet frazy są niewiele więcej niż ładną składnią, dzięki czemu funkcje mają lepsze nazwy. Zawsze trudno będzie stworzyć dobrą nazwę funkcji, jeśli wymaga ona wielu danych wejściowych.

Telastyn
źródło
Zwroty naprawdę wydają się krokiem naprzód. Wiem, że niektóre języki mają podobne możliwości, ale DALEKO od powszechnego. Nie wspominając o tym, że wszystkie nienawiści do makr pochodzące od purystów C (++), które nigdy nie używały makr poprawnie, mogą nigdy nie mieć takich funkcji w popularnych językach.
Darwin
Witaj w ogólnym temacie języków specyficznych dla domeny , coś, co naprawdę chciałbym, aby więcej osób zrozumiało zaletę ... (+1)
Izkata
2

Na przykład w Javascript (lub ECMAScript ) wielu programistów przyzwyczaiło się

przekazywanie parametrów jako zestawu nazwanych właściwości obiektu w jednym anonimowym obiekcie.

I jako praktyka programistyczna dostała się od programistów do ich bibliotek, a stamtąd do innych programistów, którzy polubili go, używają go i piszą kolejne biblioteki itp.

Przykład

Zamiast dzwonić

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

lubię to:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, który jest prawidłowym i poprawnym stylem, nazywamy go

function drawRectangleClipped (params)

lubię to:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, który jest ważny, poprawny i miły w odniesieniu do twojego pytania.

Oczywiście muszą istnieć odpowiednie warunki - w Javascripcie jest to znacznie bardziej opłacalne niż, powiedzmy, C. W javascript dało to nawet początek powszechnie stosowanej notacji strukturalnej, która stała się popularniejsza jako lżejszy odpowiednik XML. Nazywa się JSON (być może już o tym słyszałeś).

Pavel
źródło
Nie znam wystarczająco dużo tego języka, aby zweryfikować składnię, ale ogólnie podoba mi się ten post. Wydaje się dość elegancki. +1
IT Alex
Dość często łączy się to z normalnymi argumentami, tzn. Są 1-3 argumenty, po których następują params(często zawierające opcjonalne argumenty i często same opcjonalne), takie jak np. Ta funkcja . To sprawia, że ​​funkcje z wieloma argumentami są dość łatwe do zrozumienia (w moim przykładzie są 2 argumenty obowiązkowe i 6 argumentów opcji).
maaartinus
0

Powinieneś zatem użyć celu C, oto definicja funkcji:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

I tutaj jest używany:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Myślę, że Ruby ma podobne konstrukcje i można symulować je w innych językach za pomocą list kluczowych wartości.

Dla złożonych funkcji w Javie lubię definiować zmienne obojętne w brzmieniu funkcji. Na przykład z lewej i prawej:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Wygląda na więcej kodowania, ale możesz na przykład użyć leftRectangle, a następnie refaktoryzować kod później za pomocą opcji „Wyodrębnij zmienną lokalną”, jeśli uważasz, że nie będzie to zrozumiałe dla przyszłego opiekuna kodu, którym może być Ty.

awsm
źródło
O tym przykładzie Java napisałem w pytaniu, dlaczego uważam, że nie jest to dobre rozwiązanie. Co myślicie o tym?
Darwin
0

Moje podejście polega na tworzeniu tymczasowych zmiennych lokalnych - ale nie tylko wywoływaniu ich LeftRectangei RightRectangle. Używam raczej dłuższych nazw, aby przekazać więcej znaczenia. Często staram się różnicować nazwy tak bardzo, jak to możliwe, np. Nie wzywam obu something_rectangle, jeśli ich rola nie jest bardzo symetryczna.

Przykład (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

i mógłbym nawet napisać funkcję lub szablon opakowania z jedną linią:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(zignoruj ​​znaki ampersands, jeśli nie znasz C ++).

Jeśli funkcja robi kilka dziwnych rzeczy bez dobrego opisu - prawdopodobnie należy ją ponownie przefabrować!

einpoklum
źródło