Dlaczego operatory zdefiniowane przez użytkownika nie są częstsze?

94

Jedną z funkcji, których mi brakuje w językach funkcjonalnych, jest pomysł, że operatory to tylko funkcje, więc dodanie operatora niestandardowego jest często tak proste, jak dodanie funkcji. Wiele języków proceduralnych pozwala na przeciążanie operatora, więc w pewnym sensie operatory są nadal funkcjami (jest to bardzo prawdziwe w D, gdzie operator jest przekazywany jako ciąg znaków w parametrze szablonu).

Wydaje się, że tam, gdzie dozwolone jest przeciążanie operatora, dodanie dodatkowych, niestandardowych operatorów jest często banalne. Znalazłem ten post na blogu , który dowodzi, że operatorzy niestandardowi nie działają dobrze z notacją infix ze względu na reguły pierwszeństwa, ale autor podaje kilka rozwiązań tego problemu.

Rozejrzałem się i nie mogłem znaleźć języków proceduralnych, które mogłyby obsługiwać niestandardowe operatory w tym języku. Są hacki (takie jak makra w C ++), ale to prawie nie to samo, co obsługa języka.

Ponieważ ta funkcja jest dość łatwa do wdrożenia, dlaczego nie jest bardziej powszechna?

Rozumiem, że może to prowadzić do brzydkiego kodu, ale w przeszłości nie powstrzymywało to projektantów języków przed dodawaniem przydatnych funkcji, które można łatwo nadużywać (makra, operator trójskładnikowy, niebezpieczne wskaźniki).

Rzeczywiste przypadki użycia:

  • Zaimplementuj brakujące operatory (np. Lua nie ma bitowych operatorów)
  • Mimik D ~(konkatenacja macierzy)
  • DSL
  • Użyj |jako uniksowego cukru składniowego w stylu rurkowym (przy użyciu coroutines / generatorów)

Interesują mnie również języki, które pozwalają operatorom niestandardowym, ale bardziej interesuje mnie, dlaczego zostało to wykluczone. Pomyślałem o stworzeniu języka skryptowego w celu dodania operatorów zdefiniowanych przez użytkownika, ale przestałem się, gdy zdałem sobie sprawę, że nigdzie go nie widziałem, więc prawdopodobnie jest dobry powód, dla którego projektanci języków mądrzejsi ode mnie nie pozwolili.

beatgammit
źródło
4
Trwa dyskusja Reddit na temat tego pytania.
Dynamiczny
2
@dimatura: Niewiele wiem o R, ale operatorzy niestandardowi wciąż nie wydają się zbyt elastyczni (np. żaden właściwy sposób nie definiuje poprawności, zakresu, przeciążenia itp.), co wyjaśnia, dlaczego nie są często używane. W innych językach jest inaczej, z pewnością w Haskell, który bardzo często korzysta z niestandardowych operatorów poprawek . Innym przykładem języka proceduralnego z obsługą niestandardowych poprawek jest Nimrod , i oczywiście Perl również na to pozwala .
leftaroundabout
4
Jest druga opcja, Lisp nie ma właściwie różnicy między operatorami a funkcjami.
BeardedO
4
Nie wspomniano jeszcze o Prologu, innym języku, w którym operatory to tylko cukier syntaktyczny dla funkcji (tak, nawet matematycznych) i który pozwala definiować niestandardowe operatory z niestandardowym pierwszeństwem.
David Cowden
2
@BeardedO, w Lisp nie ma żadnych operatorów infix. Po ich wprowadzeniu będziesz musiał uporać się ze wszystkimi problemami w pierwszej kolejności.
SK-logic

Odpowiedzi:

134

Istnieją dwie diametralnie przeciwne szkoły myślenia w projektowaniu języka programowania. Jednym z nich jest to, że programiści piszą lepszy kod przy mniejszej liczbie ograniczeń, a drugi to, że piszą lepszy kod z większą liczbą ograniczeń. Moim zdaniem w rzeczywistości dobrzy doświadczeni programiści rozwijają się z mniejszymi ograniczeniami, ale ograniczenia te mogą poprawić jakość kodu początkujących.

Zdefiniowane przez użytkownika operatory mogą tworzyć bardzo elegancki kod w doświadczonych rękach i całkowicie okropny kod dla początkujących. Zatem to, czy Twój język je obejmuje, zależy od szkoły myślenia projektanta języka.

Karl Bielefeldt
źródło
23
W dwóch szkołach myślenia plus.google.com/110981030061712822816/posts/KaSKeg4vQtz również +1 za posiadanie najbardziej bezpośredniej (i prawdopodobnie najbardziej prawdziwej) odpowiedzi na pytanie, dlaczego projektanci języków wybierają jedną kontra drugą. Powiedziałbym również, że w oparciu o twoją tezę możesz ekstrapolować, że mniej języków na to pozwala, ponieważ mniejsza część programistów jest wysoko wykwalifikowana niż w innym przypadku (z góry procent wszystkiego jest z definicji mniejszością)
Jimmy Hoffa
12
Myślę, że ta odpowiedź jest niejasna i tylko w niewielkim stopniu związana z pytaniem. (Zdarza mi się również myśleć, że twoja charakterystyka jest błędna, ponieważ jest sprzeczna z moim doświadczeniem, ale to nie jest ważne.)
Konrad Rudolph
1
Jak powiedział @KonradRudolph, to tak naprawdę nie odpowiada na pytanie. Weź język taki jak Prolog, który pozwala ci robić wszystko, co chcesz z operatorami, w tym określać ich pierwszeństwo po umieszczeniu infix lub prefiksu. Nie sądzę, że fakt, że można pisać niestandardowe operatory, ma coś wspólnego z poziomem umiejętności docelowych odbiorców, ale raczej dlatego, że celem Prologa jest czytanie tak logicznie, jak to możliwe. Włączenie niestandardowych operatorów pozwala pisać programy, które czytają bardzo logicznie (w końcu program prologu jest tylko garstką logicznych instrukcji).
David Cowden
Jak przegapiłem ten stevey rant? O rany, zakładka do
książki
W którą filozofię wpadłbym, jeśli uważam, że programiści najprawdopodobniej napiszą najlepszy kod, jeśli języki dadzą im narzędzia do określenia, kiedy kompilator powinien wyciągać różne wnioski (np. Argumenty auto-boxowania Formatmetody) i kiedy powinien odmówić ( np. argumenty automatycznego boksu do ReferenceEquals). Im więcej umiejętności językowych daje programistom możliwość stwierdzenia, kiedy pewne wnioski będą nieodpowiednie, tym bezpieczniej może zaoferować dogodne wnioski w stosownych przypadkach.
supercat
83

Biorąc pod uwagę wybór między łączeniem tablic za pomocą ~ lub „myArray.Concat (secondArray)”, prawdopodobnie wolałbym ten drugi. Dlaczego? Ponieważ ~ jest całkowicie pozbawionym znaczenia znakiem, który ma tylko znaczenie - znaczenie łączenia tablic - podane w konkretnym projekcie, w którym został napisany.

Zasadniczo, jak powiedziałeś, operatorzy nie różnią się od metod. Ale chociaż metody mogą mieć czytelne, zrozumiałe nazwy, które zwiększają zrozumienie przepływu kodu, operatory są nieprzejrzyste i sytuacyjne.

Dlatego też nie lubię .operatora PHP (konkatenacji ciągów) ani większości operatorów w Haskell lub OCaml, choć w tym przypadku pojawiają się powszechnie akceptowane standardy dla języków funkcjonalnych.

Avner Shahar-Kashtan
źródło
27
To samo można powiedzieć o wszystkich operatorach. To, co czyni je dobrymi, a także może sprawić, że operatory zdefiniowane przez użytkownika będą dobre (jeśli zostaną zdefiniowane rozsądnie), jest takie: Pomimo tego, że użyty dla nich znak jest tylko postacią, powszechne użycie i być może właściwości mnemoniczne mają natychmiastowe znaczenie w kodzie źródłowym oczywiste dla tych, którzy go znają. Więc twoja odpowiedź w obecnej formie nie jest tak przekonująca IMHO.
33
Jak wspomniałem na końcu, niektórzy operatorzy mają uniwersalną rozpoznawalność (np. Standardowe symbole arytmetyczne, przesunięcia bitowe, AND / OR itd.), Więc ich zwięzłość przewyższa ich nieprzezroczystość. Ale zdolność definiowania dowolnych operatorów wydaje się zachowywać najgorsze z obu światów.
Avner Shahar-Kashtan
11
Czy jesteś też zwolennikiem zakazania na przykład jednoliterowych nazw na poziomie językowym? Mówiąc bardziej ogólnie, nie dopuszczając niczego w języku, który wydaje się wyrządzać więcej szkody niż pożytku? To prawidłowe podejście do projektowania języka, ale nie jedyne, więc nie sądzę, że nie można tego zakładać.
9
Nie zgadzam się, że operatorzy niestandardowi „dodają” funkcję, usuwają ograniczenia. Operatory są zwykle tylko funkcjami, więc zamiast statycznej tablicy symboli dla operatorów można zamiast tego użyć dynamicznej, zależnej od kontekstu. Wyobrażam sobie, że w ten sposób obsługiwane są przeciążenia operatora, ponieważ +i na <<pewno nie są zdefiniowane w Object(otrzymuję „brak dopasowania dla operatora + w ...”, gdy robię to na czystej klasie w C ++).
beatgammit
10
Długo trzeba się domyślić, że kodowanie zwykle nie polega na nakłonieniu komputera do zrobienia czegoś, chodzi o wygenerowanie dokumentu, który zarówno ludzie, jak i komputery mogą przeczytać, a ludzie są o wiele ważniejsi niż komputery. Ta odpowiedź jest dokładnie poprawna: jeśli następna osoba (lub ty za 2 lata) będzie musiała spędzić nawet 10 sekund, próbując dowiedzieć się, co oznacza ~, równie dobrze możesz spędzić 10 sekund na wpisaniu wywołania metody.
Bill K
71

Ponieważ ta funkcja jest dość łatwa do wdrożenia, dlaczego nie jest bardziej powszechna?

Twoje założenie jest błędne. To nie „dość trywialny do wdrożenia”. W rzeczywistości przynosi mnóstwo problemów.

Rzućmy okiem na sugerowane „rozwiązania” w poście:

  • Bez precedensu . Sam autor mówi: „Nie stosowanie reguł pierwszeństwa po prostu nie jest opcją”.
  • Semantyczne świadome parsowanie . Jak mówi artykuł, wymagałoby to od kompilatora dużej wiedzy semantycznej. Artykuł nie oferuje rozwiązania tego problemu i powiem wam, że to po prostu nie jest trywialne. Kompilatory zaprojektowano jako kompromis między mocą a złożonością. W szczególności autor wspomina o etapie wstępnej analizy w celu zebrania odpowiednich informacji, ale wstępna analiza jest nieefektywna, a kompilatory bardzo mocno starają się zminimalizować przebiegi analizy.
  • Brak niestandardowych operatorów infix . To nie jest rozwiązanie.
  • Rozwiązanie hybrydowe . To rozwiązanie niesie wiele (ale nie wszystkie) wad semantycznego świadomego analizowania. W szczególności, ponieważ kompilator musi traktować nieznane tokeny jako potencjalnie reprezentujące niestandardowe operatory, często nie może generować znaczących komunikatów o błędach. Może również wymagać zdefiniowania wspomnianego operatora, aby kontynuować parsowanie (w celu zebrania informacji o typie itp.), Po raz kolejny wymagając dodatkowej przepustki parsowania.

Podsumowując, jest to kosztowna funkcja do wdrożenia, zarówno pod względem złożoności parsera, jak i wydajności, i nie jest jasne, czy przyniosłaby wiele korzyści. Oczywiście, istnieją pewne korzyści ze zdolności definiowania nowych operatorów, ale nawet te są sporne (wystarczy spojrzeć na inne odpowiedzi argumentujące, że posiadanie nowych operatorów nie jest dobrą rzeczą).

Konrad Rudolph
źródło
2
Dzięki za głos rozsądku. Moje doświadczenie sugeruje, że ludzie odrzucający rzeczy jako trywialne są albo ekspertami, albo błogo ignorantami, a częściej w drugiej kategorii: x
Matthieu M.
14
każdy z tych problemów został rozwiązany w językach, które istnieją od 20 lat ...
Philip JF,
11
A jednak jego wdrożenie jest wyjątkowo trywialne. Zapomnij o wszystkich algorytmach analizy smoków, jest to 21 wiek i nadszedł czas, aby przejść dalej. Nawet samo rozszerzenie składni języka jest łatwe, a dodawanie operatorów o danym priorytecie jest proste. Spójrz, powiedzmy, parser Haskell. Jest to o wiele, wiele prostsze niż parsery dla „nadrzędnych” rozdętych języków.
SK-logic,
3
@ SK-logic Twoje ogólne stwierdzenie mnie nie przekonuje. Tak, parsowanie się zmieniło. Nie, implementacja arbitralnych priorytetów operatorów w zależności od typu nie jest „trywialna”. Tworzenie dobrych komunikatów o błędach w ograniczonym kontekście nie jest „trywialne”. Tworzenie wydajnych analizatorów składni z językami wymagającymi wielu przebiegów do transformacji nie jest możliwe.
Konrad Rudolph
6
W Haskell parsowanie jest „łatwe” (w porównaniu do większości języków). Pierwszeństwo jest niezależne od kontekstu. Część, w której pojawiają się trudne komunikaty o błędach, jest związana z typem i zaawansowanymi funkcjami systemu typów, a nie z operatorami zdefiniowanymi przez użytkownika. Problemem jest przeciążenie, a nie definicja użytkownika.
Philip JF
25

Zignorujmy na chwilę cały argument „operatorzy są nadużywani, by szkodzić czytelności” i skupmy się na implikacjach dotyczących projektowania języka.

Operatorzy Infix mają więcej problemów niż proste reguły pierwszeństwa (choć mówiąc wprost, odnośnik, do którego się odwołujesz, trywialnie wpływa na decyzję dotyczącą projektu). Jednym z nich jest rozwiązywanie konfliktów: co dzieje się, gdy definiujesz a.operator+(b)i b.operator+(a)? Preferowanie jednego względem drugiego prowadzi do złamania oczekiwanej właściwości przemiennej tego operatora. Zgłoszenie błędu może doprowadzić do uszkodzenia modułów, które w innym przypadku mogłyby zostać zepsute po połączeniu. Co się stanie, gdy zaczniesz wrzucać typy mieszane do miksu?

Faktem jest, że operatory to nie tylko funkcje. Funkcje są samodzielne lub należą do ich klasy, co zapewnia wyraźne preferencje, który parametr (jeśli istnieje) jest właścicielem wysyłki polimorficznej.

I to ignoruje różne problemy z pakowaniem i rozwiązywaniem, które powstają od operatorów. Powodem, dla którego projektanci języków (w zasadzie) ograniczają definicję operatora poprawki, jest to, że stwarza to wiele problemów dla języka, zapewniając jednocześnie dyskusyjne korzyści.

I szczerze mówiąc, bo oni nie trywialny do wdrożenia.

Telastyn
źródło
1
Uważam, że to jest powód, dla którego Java ich nie obejmuje, ale języki, które je mają, mają jasne zasady pierwszeństwa. Myślę, że to podobne do mnożenia macierzy. To nie jest przemienne, więc musisz być świadomy, kiedy używasz macierzy. Myślę, że to samo dotyczy zajęć z klasami, ale tak, zgadzam się, że nieporuszanie się +jest złem. Ale czy to naprawdę argument przeciwko operatorom zdefiniowanym przez użytkownika? Wydaje się to być argumentem przeciwko przeciążeniu operatora w ogóle.
beatgammit
1
@tjameson - Tak, języki mają jasne reguły dotyczące pierwszeństwa, matematyki . Reguły pierwszeństwa dla operacji matematycznych najprawdopodobniej nie będą miały zastosowania, jeśli operatorzy są przeciążeni takimi rzeczami jak boost::spirit. Gdy tylko zezwolisz operatorom zdefiniowanym przez użytkownika, sytuacja się pogorszy, ponieważ nie ma dobrego sposobu, aby nawet dobrze zdefiniować pierwszeństwo dla matematyki. Sam o tym trochę pisałem w kontekście języka, który specjalnie zajmuje się problemami z dowolnie zdefiniowanymi operatorami.
Telastyn
22
Dzwonię do bzdur, proszę pana. Poza OO-getto istnieje życie, a funkcje niekoniecznie należą do żadnego obiektu, dlatego znalezienie właściwej funkcji jest dokładnie takie samo jak znalezienie odpowiedniego operatora.
Matthieu M.,
1
@matthieuM. - Na pewno. Zasady własności i wysyłki są bardziej jednolite w tych językach, które nie obsługują funkcji członka. W pewnym momencie pierwotne pytanie brzmi: „dlaczego języki inne niż OO nie są bardziej powszechne?” co jest zupełnie inną grą.
Telastyn
11
Czy spojrzałeś na to, jak Haskell robi niestandardowe operatory? Działają dokładnie tak , jak normalne funkcje, z tym że mają również związany z nimi priorytet. (W rzeczywistości, podobnie jak normalne funkcje, więc nawet tam tak naprawdę się nie różnią). Zasadniczo operatory są domyślnie niepoprawne, a nazwy są przedrostkami, ale to jedyna różnica.
Tikhon Jelvis
19

Myślę, że byłbyś zaskoczony, jak często przeciążenia operatora realizowane w jakiejś formie. Ale nie są one powszechnie używane w wielu społecznościach.

Po co używać ~ do konkatenacji z tablicą? Dlaczego nie użyć << jak Ruby ? Ponieważ programiści, z którymi pracujesz, prawdopodobnie nie są programistami Ruby. Lub programiści D. Więc co robią, kiedy napotykają twój kod? Muszą sprawdzić, co oznacza symbol.

Kiedyś pracowałem z bardzo dobrym programistą C #, który także miał gust w językach funkcjonalnych. Niespodziewanie zaczął wprowadzać monady do C # za pomocą metod rozszerzenia i przy użyciu standardowej terminologii monad. Nikt nie mógł zaprzeczyć, że część jego kodu była bardziej przejrzysta i jeszcze bardziej czytelna, gdy tylko wiesz, co to znaczy, ale oznaczało to, że wszyscy musieli nauczyć się terminologii monad, zanim kod miał sens .

W porządku, myślisz? To był tylko mały zespół. Osobiście się nie zgadzam. Terminologia oznaczała, że ​​każdy nowy programista był zdezorientowany. Czy nie mamy wystarczających problemów z nauką nowej domeny?

Z drugiej strony, ja szczęśliwie skorzystać z ??operatora w C #, bo oczekują innych C # deweloperów wiedzieć, co to jest, ale nie przeciążać go w języku, który nie obsługuje go domyślnie.

pdr
źródło
Nie rozumiem twojego przykładu Double.NaN, to zachowanie jest częścią specyfikacji zmiennoprzecinkowej i jest obsługiwane we wszystkich używanych przeze mnie językach. Czy mówisz, że niestandardowe operatory nie są obsługiwane, ponieważ wprowadzałoby to w błąd programistów, którzy nie wiedzą o tej funkcji? To brzmi jak ten sam argument przeciwko użyciu operatora trójskładnikowego lub twojego ??przykładu.
beatgammit
@tjameson: Właściwie masz rację, to było trochę styczne do punktu, który starałem się przedstawić, i niezbyt dobrze napisane. Myślałem o ?? przykład, kiedy to pisałem i wolę ten przykład. Usunięcie akapitu double.NaN.
pdr
6
Myślę, że punkt „każdy nowy programista powinien być zdezorientowany tą terminologią” jest nieco beznadziejny, martwienie się krzywą uczenia się nowych programistów do bazy kodu jest dla mnie tym samym, co martwienie się krzywą uczenia się nowych programistów do nowa wersja .NET. Myślę, że ważniejsze jest to, kiedy deweloperzy uczą się go (nowa .NET lub twoja baza kodu), czy ma to sens i pokazuje wartość? Jeśli tak, krzywa uczenia się jest tego warta, ponieważ każdy programista musi się jej nauczyć tylko raz, chociaż kod musi być przetwarzany niezliczoną ilość razy, więc jeśli poprawi kod, będzie to niewielki koszt.
Jimmy Hoffa
7
@JimmyHoffa To powracający koszt. Płatne z góry za każdego nowego programistę, a także wpływające na istniejących programistów, ponieważ muszą go wspierać. Istnieje również koszt dokumentacji i element ryzyka - dziś wydaje się wystarczająco bezpieczny, ale po 30 latach wszyscy się posuną, język i aplikacja są teraz „starsze”, dokumentacja jest ogromną parą a niektórzy biedni frajerzy będą szarpać włosy za blask programistów, którzy byli zbyt leniwi, by wpisać „.concat ()”. Czy oczekiwana wartość wystarczy, aby zrównoważyć koszty?
Sylverdrag
3
Ponadto, argument „Każdy nowy programista powinien być zdezorientowany tą terminologią. Czy nie mamy wystarczających problemów z nauką nowej domeny?” mógł zostać zastosowany do OOP już w latach 80., wcześniej ustrukturyzowanego programowania lub IoC 10 lat temu. Czas przestać używać tego błędnego argumentu.
Mauricio Scheffer
11

Mogę wymyślić kilka powodów:

  • Nie są łatwe do zaimplementowania - zezwalanie na dowolne niestandardowe operatory mogą sprawić, że Twój kompilator będzie znacznie bardziej złożony, szczególnie jeśli zezwolisz na zdefiniowane przez użytkownika reguły pierwszeństwa, poprawności i arity. Jeśli prostota jest zaletą, przeciążenie operatora odciąga cię od dobrego projektowania języka.
  • nadużywane - głównie przez programistów, którzy uważają, że „fajnie” jest przedefiniować operatory i zacząć je przedefiniowywać dla wszystkich rodzajów niestandardowych klas. Wkrótce twój kod jest wypełniony mnóstwem niestandardowych symboli, których nikt inny nie może odczytać ani zrozumieć, ponieważ operatorzy nie przestrzegają konwencjonalnych, dobrze zrozumiałych zasad. Nie kupuję argumentu „DSL”, chyba że twój DSL jest podzbiorem matematyki :-)
  • Oni boli czytelność i łatwość konserwacji - jeśli operatorzy są regularnie zastępowane, może stać się trudne do wykrycia, gdy obiekt ten jest używany, a programiści są zmuszeni nieustannie zadawać sobie pytanie, co robi operator. O wiele lepiej jest podać sensowne nazwy funkcji. Wpisanie kilku dodatkowych znaków jest tanie, długoterminowe problemy z utrzymaniem są drogie.
  • Mogą one przełamać dorozumiane oczekiwania dotyczące wydajności . Na przykład normalnie spodziewałbym się, że będzie wyszukiwany element w tablicy O(1). Ale przy przeciążeniu operatora someobject[i]może to być O(n)operacja zależna od implementacji operatora indeksującego.

W rzeczywistości jest bardzo niewiele przypadków, w których przeciążenie operatora ma uzasadnione zastosowania w porównaniu do zwykłych funkcji. Uzasadnionym przykładem może być zaprojektowanie złożonej klasy liczb do użytku przez matematyków, którzy rozumieją dobrze rozumiane sposoby definiowania operatorów matematycznych dla liczb zespolonych. Ale to naprawdę nie jest bardzo częsty przypadek.

Kilka interesujących przypadków do rozważenia:

  • Lisps : generalnie nie rozróżnia w ogóle operatorów i funkcji - +to tylko zwykła funkcja. Możesz definiować funkcje w dowolny sposób (zazwyczaj istnieje sposób definiowania ich w oddzielnych przestrzeniach nazw, aby uniknąć konfliktu z wbudowanymi +), w tym operatorów. Ale istnieje kulturowa tendencja do używania znaczących nazw funkcji, więc nie jest to zbyt często wykorzystywane. Ponadto w prefiksie Lisp notacja zwykle się przyzwyczaja, więc w „cukrze syntaktycznym” jest mniej wartości, jaką zapewniają przeciążenia operatora.
  • Java - uniemożliwia przeciążenie operatora. Jest to czasami denerwujące (np. W przypadku liczby złożonej), ale średnio jest to prawdopodobnie właściwa decyzja projektowa dla Javy, która ma być prostym językiem OOP ogólnego przeznaczenia. Kod Java jest w rzeczywistości dość łatwy dla deweloperów o niskich / średnich umiejętnościach dzięki tej prostocie.
  • C ++ ma bardzo wyrafinowane przeciążenie operatora. Czasami jest to nadużywane ( cout << "Hello World!"ktoś?), Ale podejście ma sens, biorąc pod uwagę pozycjonowanie C ++ jako złożonego języka, który umożliwia programowanie na wysokim poziomie, jednocześnie pozwalając ci zbliżyć się do metalu pod względem wydajności, dzięki czemu możesz np. Napisać klasę liczb zespolonych, która zachowuje się dokładnie tak, jak chcesz bez utraty wydajności. Rozumie się, że to na ciebie spoczywa odpowiedzialność, jeśli postrzelisz się w stopę.
mikera
źródło
8

Ponieważ ta funkcja jest dość łatwa do wdrożenia, dlaczego nie jest bardziej powszechna?

To nie jest trywialny do wdrożenia (o ile nie trywialnie zaimplementowane). Nie zapewnia to zbyt wiele, nawet jeśli jest idealnie zaimplementowane: zyski czytelności wynikające ze zwięzłości są kompensowane przez straty czytelności wynikające z nieznajomości i zmętnienia. Krótko mówiąc, jest to rzadkie, ponieważ zwykle nie jest warte czasu programistów lub użytkowników.

To powiedziawszy, mogę myśleć o trzech językach, które to robią, i robią to na różne sposoby:

  • Rakieta, schemat, gdy nie jest to wszystko S-wyrażenie-y pozwala i oczekuje, że napiszesz parser co oznacza parser dla dowolnej składni, którą chcesz rozszerzyć (i zapewnia użyteczne haki, aby uczynić to wykonalnym).
  • Haskell, czysto funkcjonalny język programowania, pozwala zdefiniować dowolnego operatora, który składa się wyłącznie z interpunkcji, i pozwala na zapewnienie poziomu niezawodności (10 dostępnych) i asocjatywności. Operatory trójskładnikowe itp. Można tworzyć z operatorów binarnych i funkcji wyższego rzędu.
  • Agda, język programowania o typie zależnym, jest niezwykle elastyczny dzięki operatorom ( tutaj papier ), co pozwala na zdefiniowanie zarówno wtedy, jak i wtedy, gdy jeszcze, jako operatorów w tym samym programie, ale jego lexer, parser i ewaluator są ściśle powiązane w rezultacie.
Alex R.
źródło
4
Powiedziałeś, że „nie jest to trywialne”, i od razu wymieniłeś trzy języki zawierające niektóre skandalicznie trywialne implementacje. W końcu jest to dość trywialne, prawda?
SK-logic
7

Jednym z głównych powodów, dla których operatorzy niestandardowi są odradzani, jest to, że wtedy każdy operator może oznaczać / może zrobić wszystko.

Na przykład cstreambardzo krytykowane przeciążenie lewej zmiany.

Kiedy język pozwala na przeciążenie operatora, zazwyczaj zachęca się do zachowania operatora podobnego do zachowania podstawowego, aby uniknąć nieporozumień.

Również operatory zdefiniowane przez użytkownika znacznie utrudniają parsowanie, szczególnie gdy istnieją również niestandardowe reguły preferencji.

maniak zapadkowy
źródło
6
Nie rozumiem, w jaki sposób części dotyczące przeciążenia operatora odnoszą się do definiowania zupełnie nowych operatorów.
Widziałem bardzo dobre wykorzystanie przeciążenia operatora (biblioteki algebry liniowej) i bardzo złe, jak wspomniałeś. Nie sądzę, żeby to był dobry argument przeciwko niestandardowym operatorom. Analiza składniowa może stanowić problem, ale jest dość oczywista, gdy oczekuje się operatora. Po parsowaniu od generatora kodu zależy szukanie znaczenia operatora.
beatgammit
3
A czym to się różni od przeciążania metod?
Jörg W Mittag,
@ JörgWMittag z przeciążeniem metody dołączono (przez większość czasu) znaczącą nazwę. za pomocą symbolu trudniej jest wyjaśnić zwięźle, co się stanie
maniak zapadkowy
1
@ratchetfreak: Cóż. +dodaj dwie rzeczy, -odejmij je, *pomnóż je. Mam wrażenie, że nikt nie zmusza programisty do tego, aby funkcja / metoda addfaktycznie dodawała cokolwiek i doNothingmogła uruchamiać bomby atomowe. I a.plus(b.minus(c.times(d)).times(e)jest wtedy znacznie mniej czytelny a + (b - c * d) * e(dodatkowy bonus - gdzie w pierwszym żądle jest błąd w transkrypcji). Nie rozumiem, jak pierwsze ma większe znaczenie ...
Maciej Piechotka,
4

Nie używamy operatorów zdefiniowanych przez użytkownika z tego samego powodu, dla którego nie używamy słów zdefiniowanych przez użytkownika. Nikt nie nazwałby ich funkcji „sworp”. Jedynym sposobem przekazania myśli innej osobie jest użycie wspólnego języka. A to oznacza, że ​​zarówno słowa, jak i znaki (operatory) muszą być znane społeczeństwu, dla którego piszesz swój kod.

Dlatego operatory, których używasz w językach programowania, to te, których uczono nas w szkole (arytmetyka) lub te, które zostały ustanowione w społeczności programistów, jak np. Operatory logiczne.

Vagif Verdi
źródło
1
Dodaj do tego możliwość odkrycia znaczenia funkcji. W przypadku „sworp” możesz dość łatwo włożyć go do pola wyszukiwania i znaleźć jego dokumentację. Wbicie operatora w wyszukiwarkę to zupełnie inny park gry. Nasze obecne wyszukiwarki po prostu szukają operatorów, a ja traciłem dużo czasu, próbując znaleźć znaczenie w niektórych przypadkach.
Mark H
1
Ile „szkoły” liczysz? Na przykład, myślę, że ∈ elemto świetny pomysł i na pewno operator powinien zrozumieć, ale inni się nie zgadzają.
Tikhon Jelvis
1
To nie jest problem z tym symbolem. To jest problem z klawiaturą. Używam na przykład trybu emacs haskell z włączonym upiększaniem Unicode. I automatycznie konwertuje operatory ascii na symbole Unicode. Ale nie byłbym w stanie go wpisać, gdyby nie było odpowiednika ascii.
Vagif Verdi,
2
Języki naturalne zbudowane są wyłącznie z „słów zdefiniowanych przez użytkownika” i nic więcej. Niektóre języki rzeczywiście zachęcają do tworzenia niestandardowych słów.
SK-logic
4

Jeśli chodzi o języki, które obsługują takie przeciążanie: Scala robi, w rzeczywistości w znacznie czystszy i lepszy sposób może C ++. Większość znaków może być używana w nazwach funkcji, więc możesz zdefiniować operatory takie jak! + * = ++, jeśli chcesz. Istnieje wbudowane wsparcie dla poprawki (dla wszystkich funkcji pobierających jeden argument). Myślę, że można również zdefiniować asocjatywność takich funkcji. Nie możesz jednak zdefiniować pierwszeństwa (tylko z brzydkimi sztuczkami, patrz tutaj ).

kutschkem
źródło
4

Jedną z rzeczy, o których jeszcze nie wspomniano, jest sprawa Smalltalk, w której wszystko (w tym operatorzy) to wiadomość wysyłana. „Operatorzy” jak +, |i tak dalej są rzeczywiście metody unarne.

Wszystkie metody mogą zostać zastąpione, więc a + boznacza dodawanie liczb całkowitych, jeśli ai bsą liczbami całkowitymi, i oznacza dodawanie wektora, jeśli oba są liczbami całkowitymi OrderedCollection.

Nie ma żadnych reguł pierwszeństwa, ponieważ są to tylko wywołania metod. Ma to ważną implikację dla standardowej notacji matematycznej: 3 + 4 * 5znaczy (3 + 4) * 5nie 3 + (4 * 5).

(Jest to poważna przeszkoda dla początkujących Smalltalk. Łamanie reguł matematycznych usuwa specjalny przypadek, dzięki czemu cała ocena kodu przebiega jednolicie od lewej do prawej, dzięki czemu język jest o wiele prostszy.)

Frank Shearar
źródło
3

Walczysz tutaj z dwiema rzeczami:

  1. Dlaczego w ogóle operatorzy istnieją w językach?
  2. Jaka jest zaleta operatorów nad funkcjami / metodami?

W większości języków operatory nie są tak naprawdę implementowane jako proste funkcje. Mogą mieć pewne rusztowania funkcyjne, ale kompilator / środowisko wykonawcze jest wyraźnie świadome ich znaczenia semantycznego i tego, jak skutecznie je przetłumaczyć na kod maszynowy. Jest to o wiele bardziej prawdziwe, nawet w porównaniu z funkcjami wbudowanymi (dlatego większość implementacji nie obejmuje również wszystkich narzutów wywołania funkcji w swojej implementacji). Większość operatorów to abstrakcje wyższego poziomu w prymitywnych instrukcjach znalezionych w procesorach (częściowo dlatego większość operatorów jest arytmetyczna, logiczna lub bitowa). Możesz modelować je jako „specjalne” funkcje (nazwać je „prymitywami”, „wbudowanymi” lub „natywnymi” lub cokolwiek innego), ale aby to zrobić ogólnie, wymaga bardzo solidnego zestawu semantyki do definiowania takich funkcji specjalnych. Alternatywą jest posiadanie wbudowanych operatorów, które semantycznie wyglądają jak operatory zdefiniowane przez użytkownika, ale które w innym przypadku wywołują specjalne ścieżki w kompilatorze. Odpowiada to odpowiedzi na drugie pytanie ...

Oprócz wspomnianego wyżej problemu tłumaczenia maszynowego, na poziomie syntaktycznym operatorzy tak naprawdę nie różnią się od funkcji. Cechami wyróżniającymi są zazwyczaj zwięzłe i symboliczne, co wskazuje na istotną dodatkową cechę, że muszą być przydatne: muszą mieć szeroko rozumiane znaczenie / semantykę dla programistów. Krótkie symbole nie mają większego znaczenia, chyba że jest to krótka ręka dla zestawu semantyki, który jest już zrozumiany. To sprawia, że ​​operatory zdefiniowane przez użytkownika są z natury nieprzydatne, ponieważ ze swej natury nie są tak szeroko rozumiane. Mają tyle samo sensu, co jedna lub dwie litery funkcji.

Przeciążenia operatora C ++ zapewniają żyzny grunt do badania tego. Większość „nadużyć” przeciążających operatorów występuje w postaci przeciążeń, które łamią część szeroko rozumianego kontraktu semantycznego (klasyczny przykład to przeciążenie operatora + takie, że a + b! = B + a lub gdzie + modyfikuje którykolwiek z jego operandy).

Jeśli spojrzysz na Smalltalk, który pozwala na przeciążanie operatorów i operatorów zdefiniowanych przez użytkownika, możesz zobaczyć, jak język może to robić i jak użyteczne. W Smalltalk operatory są jedynie metodami o różnych właściwościach składniowych (mianowicie są zakodowane jako binarne infix). Język używa „prymitywnych metod” dla specjalnych przyspieszonych operatorów i metod. Okazuje się, że niewiele jest utworzonych operatorów zdefiniowanych przez użytkownika, a kiedy już są, zwykle nie są przyzwyczajeni tak często, jak autor prawdopodobnie zamierzał je wykorzystać. Nawet ekwiwalent przeciążenia operatora jest rzadki, ponieważ definiowanie nowej funkcji jako operatora zamiast metody jest głównie stratą netto, ponieważ ta ostatnia pozwala na wyrażenie semantyki funkcji.

Christopher Smith
źródło
1

Zawsze uważałem, że przeciążenia operatorów w C ++ są wygodnym skrótem dla zespołu jednego dewelopera, ale powoduje to wszelkiego rodzaju zamieszanie w dłuższej perspektywie, po prostu dlatego, że wywołania metod są „ukryte” w sposób, który nie jest łatwy dla narzędzi takich jak doxygen, aby rozdzielić, a ludzie muszą zrozumieć idiomy, aby właściwie z nich korzystać.

Czasami jest to o wiele trudniejsze do zrozumienia, niż można się było spodziewać. Dawno, dawno temu, w dużym międzyplatformowym projekcie C ++ zdecydowałem, że dobrym pomysłem byłoby znormalizowanie sposobu budowania ścieżek poprzez utworzenie FilePathobiektu (podobnego do Fileobiektu Javy ), który miałby operator / użyłby do połączenia innego ścieżka na nim (abyś mógł zrobić coś takiego File::getHomeDir()/"foo"/"bar"i zrobiłby to dobrze na wszystkich obsługiwanych platformach). Każdy, kto to widział, powiedziałby w zasadzie: „Co do diabła? Podział strun? ... Och, to urocze, ale nie ufam, że zrobi to dobrze”.

Podobnie jest wiele przypadków w programowaniu graficznym lub w innych obszarach, w których matematyka wektorowa / macierzowa zdarza się często, w których kuszące jest robienie takich rzeczy jak Matrix * Matrix, Vector * Vector (dot), Vector% Vector (cross), Matrix * Vector ( transformacja macierzowa), macierz ^ wektor (transformacja macierzowa specjalnego przypadku ignorująca jednorodną współrzędną - przydatną w przypadku normalnych powierzchni) i tak dalej, ale chociaż oszczędza to trochę czasu analizy dla osoby, która napisała bibliotekę matematyki wektorowej, kończy się tylko nawet bardziej myląc ten problem dla innych. Po prostu nie jest tego warte.

puszysty
źródło
0

Przeciążenia operatora są złym pomysłem z tego samego powodu, że przeciążenia metod są złym pomysłem: ten sam symbol na ekranie miałby różne znaczenie w zależności od tego, co jest wokół. Utrudnia to swobodne czytanie.

Ponieważ czytelność jest kluczowym aspektem łatwości konserwacji, należy zawsze unikać przeciążenia (z wyjątkiem bardzo szczególnych przypadków). O wiele lepiej jest, aby każdy symbol (czy to operator, czy identyfikator alfanumeryczny) miał unikalne znaczenie.

Przykład: podczas czytania nieznanego kodu, jeśli napotkasz nowy identyfikator alfanum, którego nie znasz, przynajmniej masz tę przewagę, że wiesz, że go nie znasz. Następnie możesz to sprawdzić. Jeśli jednak widzisz wspólny identyfikator lub operator, którego znasz znaczenie, znacznie mniej prawdopodobne jest, że zauważysz, że w rzeczywistości został przeciążony i ma zupełnie inne znaczenie. Aby wiedzieć, którzy operatorzy zostali przeciążeni (w bazie kodu, która szeroko wykorzystuje przeciążenie), potrzebujesz praktycznej wiedzy na temat całego kodu, nawet jeśli chcesz tylko przeczytać jego niewielką część. Utrudniłoby to przyspieszenie nowych programistów tego kodu i uniemożliwiłoby ludziom rozpoczęcie drobnej pracy. Może to być dobre dla bezpieczeństwa pracy programisty, ale jeśli jesteś odpowiedzialny za sukces bazy kodu, powinieneś unikać tej praktyki za wszelką cenę.

Ponieważ operatory są małych rozmiarów, przeciążanie operatorów pozwoliłoby na gęstszy kod, ale zwiększenie gęstości kodu nie jest prawdziwą korzyścią. Czytanie linii o podwójnej logice zajmuje dwa razy więcej. Kompilator nie dba o to. Jedynym problemem jest czytelność dla ludzi. Ponieważ kompaktowanie kodu nie poprawia czytelności, kompaktowość nie ma realnych korzyści. Śmiało, zajmij miejsce i nadaj unikalnym operacjom unikalny identyfikator, a Twój kod odniesie większy sukces na dłuższą metę.

AgilePro
źródło
„ten sam symbol na ekranie miałby różne znaczenie w zależności od tego, co jest wokół” - tak jest już w przypadku wielu operatorów w większości języków.
rkj
-1

Pomijając trudności techniczne w radzeniu sobie z pierwszeństwem i złożoną analizą składni, myślę, że istnieje kilka aspektów tego, czym jest język programowania, który należy wziąć pod uwagę.

Operatory to zazwyczaj krótkie logiczne konstrukcje, które są dobrze zdefiniowane i udokumentowane w języku podstawowym (porównaj, przypisz ...). Są też zwykle trudne do zrozumienia bez dokumentacji (porównać a^bz xor(a,b)na przykład). Istnieje raczej ograniczona liczba operatorów, które mogą mieć sens w normalnym programowaniu (>, <, =, + itd.).

Mój pomysł jest taki, że lepiej trzymać się zestawu dobrze zdefiniowanych operatorów w języku - następnie pozwolić operatorowi na przeciążenie tych operatorów (biorąc pod uwagę miękkie zalecenie, że operatorzy powinni robić to samo, ale z niestandardowym typem danych).

Twoje przypadki użycia ~i |byłyby możliwe przy prostym przeciążeniu operatora (C #, C ++ itp.). DSL jest prawidłowym obszarem użytkowania, ale prawdopodobnie jednym z niewielu ważnych obszarów (z mojego punktu widzenia). Myślę jednak, że istnieją lepsze narzędzia do tworzenia nowych języków. Wykonanie prawdziwego języka DSL w innym języku nie jest takie trudne przy użyciu dowolnego z tych narzędzi kompilatora-kompilatora. To samo dotyczy „rozszerzenia argumentu LUA”. Język jest najprawdopodobniej zdefiniowany przede wszystkim w celu rozwiązywania problemów w określony sposób, a nie jako podstawa dla podjęzyków (istnieją wyjątki).

Petter Nordlander
źródło
-1

Innym czynnikiem jest to, że zdefiniowanie operacji z dostępnymi operatorami nie zawsze jest proste. Mam na myśli, tak, dla każdego rodzaju liczby operator „*” może mieć sens i zwykle jest implementowany w języku lub w istniejących modułach. Ale w przypadku typowych złożonych klas, które należy zdefiniować (takie jak ShipingAddress, WindowManager, ObjectDimensions, PlayerCharacter itp.), Takie zachowanie nie jest jasne ... Co oznacza dodawanie lub odejmowanie liczby do adresu? Czy pomnożyć dwa adresy?

Jasne, możesz zdefiniować, że dodanie łańcucha do klasy ShippingAddress oznacza niestandardową operację, taką jak „zamień wiersz 1 w adresie” (zamiast funkcji „setLine1”), a dodanie liczby to „zamień kod pocztowy” (zamiast „setZipCode”) , ale kod nie jest zbyt czytelny i mylący. Zazwyczaj uważamy, że operatory są używane w podstawowych typach / klasach, ponieważ ich zachowanie jest intuicyjne, jasne i spójne (przynajmniej po zaznajomieniu się z językiem). Pomyśl w typach takich jak Liczba całkowita, Ciąg, Liczba złożona itp.

Zatem nawet jeśli zdefiniowanie operatorów może być bardzo przydatne w niektórych konkretnych przypadkach, ich rzeczywiste wdrożenie jest dość ograniczone, ponieważ 99% przypadków, w których będzie to oczywiste zwycięstwo, zostało już zaimplementowanych w pakiecie podstawowego języka.

Khelben
źródło