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.
źródło
Odpowiedzi:
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.
źródło
Format
metody) i kiedy powinien odmówić ( np. argumenty automatycznego boksu doReferenceEquals
). 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.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.źródło
+
i na<<
pewno nie są zdefiniowane wObject
(otrzymuję „brak dopasowania dla operatora + w ...”, gdy robię to na czystej klasie w C ++).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:
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ą).
źródło
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)
ib.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.
źródło
+
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.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.Myślę, że byłbyś zaskoczony, jak często przeciążenia operatora są 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.źródło
??
przykładu.Mogę wymyślić kilka powodów:
O(1)
. Ale przy przeciążeniu operatorasomeobject[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:
+
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.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ę.źródło
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:
źródło
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
cstream
bardzo 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.
źródło
+
dodaj dwie rzeczy,-
odejmij je,*
pomnóż je. Mam wrażenie, że nikt nie zmusza programisty do tego, aby funkcja / metodaadd
faktycznie dodawała cokolwiek idoNothing
mogła uruchamiać bomby atomowe. Ia.plus(b.minus(c.times(d)).times(e)
jest wtedy znacznie mniej czytelnya + (b - c * d) * e
(dodatkowy bonus - gdzie w pierwszym żądle jest błąd w transkrypcji). Nie rozumiem, jak pierwsze ma większe znaczenie ...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.
źródło
elem
to świetny pomysł i na pewno operator powinien zrozumieć, ale inni się nie zgadzają.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 ).
źródło
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 + b
oznacza dodawanie liczb całkowitych, jeślia
ib
są liczbami całkowitymi, i oznacza dodawanie wektora, jeśli oba są liczbami całkowitymiOrderedCollection
.Nie ma żadnych reguł pierwszeństwa, ponieważ są to tylko wywołania metod. Ma to ważną implikację dla standardowej notacji matematycznej:
3 + 4 * 5
znaczy(3 + 4) * 5
nie3 + (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.)
źródło
Walczysz tutaj z dwiema rzeczami:
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.
źródło
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
FilePath
obiektu (podobnego doFile
obiektu Javy ), który miałby operator / użyłby do połączenia innego ścieżka na nim (abyś mógł zrobić coś takiegoFile::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.
źródło
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ę.
źródło
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^b
zxor(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).źródło
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.
źródło