Czytałem artykuł tutaj: http://www.paulgraham.com/avg.html, a część o „paradoksie blub” była szczególnie interesująca. Jako ktoś, kto głównie koduje w c ++, ale ma kontakt z innymi językami (głównie Haskell), jestem świadomy kilku przydatnych rzeczy w tych językach, które trudno jest powielić w c ++. Pytanie skierowane jest głównie do osób biegle posługujących się zarówno językiem c ++, jak i jakimś innym językiem. Czy istnieje jakaś potężna funkcja lub idiom językowy, którego używasz w języku, który trudno byłoby pojąć lub wdrożyć, gdybyś pisał tylko w języku c ++?
W szczególności ten cytat przykuł moją uwagę:
Dzięki indukcji jedynymi programistami, którzy mogą zobaczyć wszystkie różnice w mocy między różnymi językami, są ci, którzy rozumieją ten najsilniejszy. (Prawdopodobnie to właśnie miał na myśli Eric Raymond o tym, że Lisp uczynił cię lepszym programistą.) Nie możesz ufać opiniom innych, z powodu paradoksu Blub: są zadowoleni z dowolnego języka, którego używają, ponieważ to dyktuje sposób, w jaki myślą o programach.
Jeśli okaże się, że jestem odpowiednikiem programisty „Blub” dzięki użyciu c ++, powstaje następujące pytanie: Czy są jakieś użyteczne koncepcje lub techniki, które napotkałeś w innych językach, które trudno byłoby ci pojąć, gdybyś pisałeś lub „myślałeś” w c ++?
Na przykład paradygmat programowania logicznego widziany w językach takich jak Prolog i Mercury można zaimplementować w c ++ przy użyciu biblioteki castor, ale ostatecznie uważam, że koncepcyjnie myślę w kategoriach kodu Prolog i tłumacząc na odpowiednik c ++ podczas korzystania z tego. W celu poszerzenia mojej wiedzy programistycznej próbuję dowiedzieć się, czy istnieją inne podobne przykłady użytecznych / potężnych idiomów, które są bardziej efektywnie wyrażane w innych językach, o których być może nie jestem świadomy jako programista c ++. Innym przykładem, który przychodzi mi na myśl, jest system makr w lisp, generowanie kodu programu z poziomu programu wydaje się mieć wiele zalet w przypadku niektórych problemów. Wydaje się to trudne do zaimplementowania i przemyślenia z poziomu c ++.
To pytanie nie ma być debatą „c ++ vs lisp” ani żadną debatą typu wojny językowe. Zadanie takiego pytania to jedyny sposób, w jaki mogę dowiedzieć się o rzeczach, o których nie wiem, o których nie wiem.
źródło
there are things that other languages can do that Lisp can't
- Mało prawdopodobne, ponieważ Lisp jest ukończony w Turingu . Być może chciałeś powiedzieć, że są pewne rzeczy, które nie są praktyczne w Lisp? Mógłbym powiedzieć to samo o każdym języku programowania.Odpowiedzi:
Odkąd wspominałeś o Haskell:
Dopasowywanie wzorów. Uważam, że dopasowanie wzorca jest o wiele łatwiejsze do czytania i pisania. Zastanów się nad definicją mapy i zastanów się, w jaki sposób zostanie ona zaimplementowana w języku bez dopasowania wzorca.
System typów. Czasami może to być ból, ale jest niezwykle pomocny. Musisz się z nim zaprogramować, aby naprawdę to zrozumieć i ile błędów wyłapie. Również przejrzystość referencyjna jest cudowna. Po programowaniu w Haskell staje się widoczne tylko przez chwilę, ile błędów jest spowodowanych zarządzaniem stanem w imperatywnym języku.
Ogólnie programowanie funkcjonalne. Używanie map i foldów zamiast iteracji. Rekurencja Chodzi o myślenie na wyższym poziomie.
Leniwa ocena. Znów chodzi o myślenie na wyższym poziomie i umożliwienie systemowi oceny.
Cabal, pakiety i moduły. Posiadanie pakietów Cabala dla mnie jest o wiele wygodniejsze niż znajdowanie kodu źródłowego, pisanie makefile itp. Możliwość importowania tylko niektórych nazw jest znacznie lepsza niż zasadniczo zrzucenie wszystkich plików źródłowych, a następnie skompilowanie.
źródło
Maybe
(dla C ++ patrzstd::optional
), chodzi o jawne oznaczenie rzeczy jako opcjonalne / nullable / może.Zapamiętaj!
Spróbuj napisać w C ++. Nie z C ++ 0x.
Zbyt kłopotliwy? Okej, spróbuj z C ++ 0x.
Sprawdź, czy możesz pokonać tę 4-liniową (lub 5-liniową, cokolwiek: P) wersję kompilacji w D:
Aby zadzwonić, wystarczy tylko:
Możesz także wypróbować coś podobnego w schemacie, chociaż jest to nieco wolniejsze, ponieważ dzieje się to w czasie wykonywania i ponieważ wyszukiwanie tutaj jest liniowe zamiast mieszania (i cóż, ponieważ jest to schemat):
źródło
C ++ to język oparty na wielu algorytmach, co oznacza, że próbuje obsługiwać wiele sposobów myślenia. Czasami funkcja C ++ jest bardziej niezręczna lub mniej płynna niż implementacja innego języka, jak ma to miejsce w przypadku programowania funkcjonalnego.
To powiedziawszy, nie mogę oderwać od głowy natywnej funkcji języka C ++, która robi to, co robi
yield
w Pythonie lub JavaScript.Innym przykładem jest programowanie równoległe . C ++ 0x ma coś do powiedzenia na ten temat, ale obecny standard tego nie robi, a współbieżność jest zupełnie nowym sposobem myślenia.
Ponadto szybki rozwój - nawet programowanie w powłoce - jest czymś, czego nigdy się nie nauczysz, jeśli nigdy nie opuścisz domeny programowania w C ++.
źródło
setjmp
ilongjmp
. Nie mam pojęcia, ile to się psuje, ale chyba pierwsze byłyby wyjątki. Teraz, jeśli mi wybaczysz, muszę ponownie przeczytać Modern C ++ Design, aby wymyślić to z mojej głowy.Coroutines to niezwykle przydatna funkcja językowa, która stanowi podstawę wielu bardziej namacalnych zalet innych języków w stosunku do C ++. Zasadniczo zapewniają dodatkowe stosy, dzięki czemu funkcje mogą być przerywane i kontynuowane, zapewniając językowi podobne do potoków funkcje, które z łatwością przekazują wyniki operacji przez filtry do innych operacji. To wspaniałe, aw Ruby uważam to za bardzo intuicyjne i eleganckie. Leniwa ocena również się z tym wiąże.
Introspekcja i kompilacja / wykonanie / ocena kodu w czasie wykonywania to cokolwiek, co stanowi potężne funkcje, których brakuje w C ++.
źródło
Po wdrożeniu systemu algebry komputerowej zarówno w Lisp, jak i C ++, mogę powiedzieć, że zadanie było znacznie łatwiejsze w Lisp, mimo że byłem kompletnym nowicjuszem w tym języku. Ta uproszczona natura wszystkich list upraszcza wiele algorytmów. To prawda, że wersja C ++ była zillion razy szybsza. Tak, mogłem przyspieszyć wersję lisp, ale kod nie byłby tak lispy. Na przykład skrypty to kolejna rzecz, która zawsze będzie łatwiejsza, to seplenienie. Chodzi o użycie odpowiedniego narzędzia do pracy.
źródło
Co mamy na myśli, mówiąc, że jeden język jest „potężniejszy” niż inny? Kiedy mówimy, że język jest „ekspresyjny”? Lub „bogaty”? Myślę, że mamy na myśli, że język zyskuje na sile, gdy jego pole widzenia jest wystarczająco wąskie, aby łatwo i naturalnie opisać problem - tak naprawdę zmiana stanu, prawda? - które jest zgodne z tym poglądem. Jednak ten język jest znacznie mniej wydajny, mniej wyrazisty i mniej przydatny, gdy nasze pole widzenia się poszerzy.
Im bardziej „mocny” i „wyrazisty” jest język, tym bardziej ograniczone jest jego użycie. Może więc „potężny” i „ekspresyjny” to niewłaściwe słowa, których można użyć w przypadku wąskiego narzędzia. Może „odpowiednie” lub „abstrakcyjne” to lepsze słowa na takie rzeczy.
Zacząłem od programowania, pisząc całą masę rzeczy niskiego poziomu: sterowniki urządzeń z ich procedurami przerwania; programy osadzone; kod systemu operacyjnego. Kod był ściśle związany ze sprzętem i napisałem to wszystko w języku asemblera. Nie powiedzielibyśmy, że asembler jest w najmniej abstrakcyjny sposób, a jednak był i jest najpotężniejszym i najbardziej wyrazistym językiem ze wszystkich. Potrafię wyrazić każdy problem w języku asemblera; jest tak potężny, że mogę zrobić wszystko, co mi się podoba z dowolną maszyną.
Całe moje późniejsze rozumienie języka wyższego poziomu zawdzięcza wszystko mojemu doświadczeniu z asemblerem. Wszystko, czego nauczyłem się później, było łatwe, ponieważ, jak widać, wszystko - bez względu na to, jak abstrakcyjne - musi ostatecznie dostosować się do sprzętu.
Możesz zapomnieć o coraz wyższych poziomach abstrakcji - czyli węższych i węższych polach widzenia. Zawsze możesz to odebrać później. Uczenie się w mgnieniu oka to kwestia dni. Moim zdaniem lepiej byłoby uczyć się języka sprzętu 1 , aby zbliżyć się jak najdalej do kości.
1 Być może nie dość niemądry, ale
car
icdr
biorę ich nazwy od sprzętu: pierwszy Lisp działał na maszynie, która miała rzeczywisty Rejestr Decrement i rzeczywisty Rejestr Adresowy. Co powiesz na to?źródło
Tablice asocjacyjne
Typowy sposób przetwarzania danych to:
Właściwym narzędziem do tego jest tablica asocjacyjna .
Nie podoba mi się składniowa tablica asocjacyjna JavaScript, ponieważ nie mogę utworzyć, powiedzmy [x] [y] [z] = 8 , najpierw muszę utworzyć [x] i [x] [y] .
Okej, w C ++ (i Javie) jest całkiem fajne portfolio klas kontenerów, Map , Multimap , cokolwiek, ale jeśli chcę skanować, muszę zrobić iterator, a kiedy chcę wstawić nowy element głęboki, ja trzeba stworzyć wszystkie wyższe poziomy itp. Niewygodne.
Nie twierdzę, że nie ma użytecznych tablic asocjacyjnych w C ++ (i Javie), ale języki skryptowe bez czcionek (lub nie o ścisłym typowaniu) biją skompilowane, ponieważ są to języki bez czcionek.
Oświadczenie: Nie znam C # i innych języków .NET, AFAIK mają dobrą obsługę tablicy asocjacyjnej.
źródło
dict
typu (np.x = {0: 5, 1: "foo", None: 500e3}
Pamiętaj, że nie ma wymogu, aby klucze lub wartości były tego samego typu). Próba zrobienia czegoś podobnegoa[x][y][z] = 8
jest trudna, ponieważ język musi patrzeć w przyszłość, aby sprawdzić, czy zamierzasz ustawić wartość lub stworzyć inny poziom; samo wyrażeniea[x][y]
tego nie mówi.Nie uczę się Java, C \ C ++, asemblera i Java Script. Używam C ++ do zarabiania na życie.
Chociaż muszę powiedzieć, że bardziej lubię programowanie asemblacyjne i programowanie C. Jest to związane głównie z programowaniem imperatywnym.
Wiem, że paradygmaty programowania są ważne, aby kategoryzować typy danych i dawać abstrakcyjne koncepcje programowania, aby umożliwić wydajne wzorce projektowe i formalizację kodu. Chociaż w pewnym sensie, każdy Paradygmat jest zbiorem wzorców i kolekcji w celu wyodrębnienia podstawowej warstwy sprzętowej, abyś nie musiał myśleć o EAX lub IP wewnętrznie w maszynie.
Moim jedynym problemem jest to, że pozwala to, aby pojęcie i koncepcje działania maszyny zostały przekształcone w ideologię i dwuznaczne twierdzenia o tym, co się dzieje. Ten chleb zawiera wszelkiego rodzaju wspaniałe abstrakty oprócz streszczeń do jakiegoś ideologicznego celu programisty.
Pod koniec dnia lepiej jest mieć jasne podejście i granice dotyczące tego, czym jest procesor i jak działają komputery pod maską. Wszystkim, którym zależy na CPU, jest wykonanie szeregu instrukcji, które przenoszą dane do pamięci i do pamięci do rejestru i wykonują instrukcje. Nie ma pojęcia typu danych ani żadnych wyższych koncepcji programistycznych. To tylko przenosi dane.
Staje się to bardziej skomplikowane, gdy dodasz do miksu paradygmaty programowania, ponieważ nasze spojrzenie na świat jest inne.
źródło
C ++ sprawia, że wiele podejść jest trudnych. Posunąłbym się nawet do stwierdzenia, że większość programowania jest trudna do konceptualizacji, jeśli ograniczysz się do C ++. Oto kilka przykładów problemów, które można łatwiej rozwiązać w sposób utrudniający C ++.
Zarejestruj konwencje przydziału i połączeń
Wiele osób myśli o C ++ jako języku niskiego poziomu, ale tak naprawdę nie jest. Wyodrębniając ważne szczegóły maszyny, C ++ utrudnia konceptualizację praktycznych aspektów, takich jak przydzielanie rejestrów i konwencje wywoływania.
Aby dowiedzieć się więcej o takich koncepcjach, polecam programowanie w asemblerze i zapoznaj się z tym artykułem na temat jakości generowania kodu ARM .
Generowanie kodu w czasie wykonywania
Jeśli znasz tylko C ++, prawdopodobnie myślisz, że szablony są najważniejszym metaprogramowaniem. Nie są. W rzeczywistości są obiektywnie złym narzędziem do metaprogramowania. Każdy program, który manipuluje innym programem, jest metaprogramem, w tym interpreterami, kompilatorami, komputerowymi systemami algebry i dowodami twierdzeń. Przydatne jest do tego generowanie kodu w czasie wykonywania.
Polecam odpalenie implementacji schematu i zabawę,
EVAL
aby dowiedzieć się o ewaluacji śródkolistej.Manipulowanie drzewami
Drzewa są wszędzie w programowaniu. Podczas parsowania masz abstrakcyjne drzewa składniowe. W kompilatorach masz IR, które są drzewami. W grafice i programowaniu GUI masz drzewa scen.
Ten „śmiesznie prosty parser JSON dla C ++” waży zaledwie 484 LOC, co jest bardzo małe dla C ++. Teraz porównaj to z moim prostym parserem JSON, który waży zaledwie 60 LOC F #. Różnica polega przede wszystkim na tym, że algebraiczne typy danych ML i dopasowywanie wzorców (w tym aktywne wzorce) znacznie ułatwiają manipulowanie drzewami.
Sprawdź także czerwono-czarne drzewa w OCaml .
Czysto funkcjonalne struktury danych
Brak GC w C ++ praktycznie uniemożliwia przyjęcie pewnych przydatnych podejść. Czysto funkcjonalne struktury danych są jednym z takich narzędzi.
Na przykład sprawdź ten 47-liniowy moduł dopasowywania wyrażeń regularnych w OCaml. Zwięzłość wynika głównie z szerokiego zastosowania czysto funkcjonalnych struktur danych. W szczególności korzystanie ze słowników z kluczami, które są zestawami. Jest to naprawdę trudne w C ++, ponieważ wszystkie słowniki i zestawy stdlib są zmienne, ale nie można mutować kluczy słownika lub psujesz kolekcję.
Programowanie logiczne i bufory cofania są innymi praktycznymi przykładami, w których czysto funkcjonalne struktury danych sprawiają, że coś trudnego w C ++ jest naprawdę łatwe w innych językach.
Woła ogon
C ++ nie tylko nie gwarantuje wywołania ogona, ale RAII jest zasadniczo w sprzeczności z tym, ponieważ destruktory przeszkadzają w wywołaniu ogona. Wywołania ogona umożliwiają wykonywanie nieograniczonej liczby wywołań funkcji przy użyciu ograniczonej ilości miejsca na stosie. Jest to świetne rozwiązanie do implementacji automatów stanowych, w tym rozszerzalnych automatów stanowych, i jest świetną kartą „wyjdź z więzienia” w wielu innych niezręcznych okolicznościach.
Na przykład sprawdź tę implementację problemu plecakowego 0-1, używając stylu kontynuacji przekazywania z zapamiętywaniem w języku F # od branży finansowej. Gdy masz wywołania ogona, styl przekazywania kontynuacji może być oczywistym rozwiązaniem, ale C ++ czyni go trudnym do rozwiązania.
Konkurencja
Innym oczywistym przykładem jest jednoczesne programowanie. Chociaż jest to całkowicie możliwe w C ++, jest bardzo podatne na błędy w porównaniu z innymi narzędziami, w szczególności komunikując procesy sekwencyjne, jak widać w językach takich jak Erlang, Scala i F #.
źródło
To stare pytanie, ale ponieważ nikt o nim nie wspominał, dodam listę (i teraz dyktuję) wyrażenia. Łatwo jest napisać linijkę w języku Haskell lub Python, która rozwiązuje problem Fizz-Buzz. Spróbuj to zrobić w C ++.
Podczas gdy C ++ dokonał ogromnych ruchów do nowoczesności za pomocą C ++ 11, trudno jest nazwać go „nowoczesnym” językiem. C ++ 17 (który nie został jeszcze wydany) robi jeszcze więcej ruchów, aby sprostać współczesnym standardom, o ile „nowoczesny” oznacza „nie z poprzedniego tysiąclecia”.
Nawet najprostsze ze słów, które można napisać w jednym wierszu w Pythonie (i przestrzeganie limitu długości linii znaków wynoszącego 79 znaków) stają się mnóstwem wierszy kodów po przetłumaczeniu na C ++, a niektóre z tych wierszy kodu C ++ są raczej skomplikowane.
źródło
Skompilowana biblioteka wywołująca wywołanie zwrotne, które jest zdefiniowaną przez użytkownika funkcją składową klasy zdefiniowanej przez użytkownika.
Jest to możliwe w Objective-C i sprawia, że programowanie interfejsu użytkownika jest dziecinnie proste. Możesz powiedzieć przyciskowi: „Po naciśnięciu wywołaj tę metodę dla tego obiektu”, a przycisk zrobi to. Możesz dowolnie używać dowolnej nazwy metody dla wywołania zwrotnego, która ci się podoba, nie jest zawieszona w kodzie biblioteki, nie musisz dziedziczyć po adapterze, aby działała, ani kompilator nie chce rozwiązać wywołania w czasie kompilacji, i, co równie ważne, możesz powiedzieć dwóm przyciskom, aby wywoływały dwie różne metody tego samego obiektu.
Nie widziałem jeszcze tak elastycznego sposobu definiowania oddzwaniania w żadnym innym języku (choć bardzo chciałbym o nich usłyszeć!). Najbliższym odpowiednikiem w C ++ jest prawdopodobnie przekazanie funkcji lambda, która wykonuje wymagane wywołanie, co ponownie ogranicza kod biblioteki jako szablon.
To właśnie ta cecha Celu C nauczyła mnie doceniać zdolność języka do przekazywania dowolnego rodzaju obiektów / funkcji / cokolwiek-ważnego-pojęcia-języka-zawiera się swobodnie, wraz z mocą zapisywania ich do zmienne. Każdy punkt w języku, który definiuje dowolny rodzaj pojęcia, ale nie zapewnia sposobu przechowywania go (lub odniesienia do niego) we wszystkich dostępnych rodzajach zmiennych, jest znaczącą przeszkodą i prawdopodobnie źródłem wielu brzydkich, zduplikowany kod. Niestety, barokowe języki programowania mają zwykle wiele z następujących cech:
W C ++ nie można zapisać typu VLA ani przechowywać do niego wskaźnika. To skutecznie zabrania prawdziwych wielowymiarowych tablic o dynamicznym rozmiarze (które są dostępne w C od C99).
W C ++ nie można zapisać typu lambda. Nie możesz nawet tego wpisać. Tak więc nie ma sposobu na ominięcie lambdy lub zapisanie odniesienia do niej w obiekcie. Funkcje Lambda można przekazać tylko do szablonów.
W Fortran nie można zapisać typu listy nazw. Po prostu nie ma sposobu na przekazanie listy nazw do jakiejkolwiek procedury. Jeśli masz złożony algorytm, który powinien być w stanie obsłużyć dwie różne listy nazw, nie masz szczęścia. Nie można po prostu napisać algorytmu raz i przekazać mu odpowiednich list nazw.
To tylko kilka przykładów, ale widzisz wspólny punkt: ilekroć zobaczysz takie ograniczenie po raz pierwszy, zwykle nie będziesz się tym przejmować, ponieważ robienie zakazanej rzeczy wydaje się takie szalone. Jednak, gdy robisz poważne programowanie w tym języku, w końcu dochodzisz do punktu, w którym to dokładne ograniczenie staje się prawdziwą uciążliwością.
źródło
I have not seen a similarly flexible way to define a callback in any other language yet (though I'd be very interested to hear about them!)
To, co właśnie opisujesz, brzmi dokładnie tak, jak działa kod interfejsu użytkownika sterowany zdarzeniami w Delphi. (Oraz w .NET WinForms, na który Delphi miał duży wpływ.)std::vector
. Chociaż jest nieco mniej wydajny ze względu na nieużywanie alokacji stosu, jest funkcjonalnie izomorficzny dla VLA, więc tak naprawdę nie liczy się jako problem typu „blub”: programiści C ++ mogą sprawdzić, jak to działa i po prostu powiedzieć „ah tak , C robi to wydajniej niż C ++ ”.std::function
jest.object::method
i zostanie on przekonwertowany na instancję dowolnego interfejsu, którego oczekuje kod odbiorczy. C # ma delegatów. Każdy obiektowo-funkcjonalny język ma tę funkcję, ponieważ jest to zasadniczo punkt przekroju dwóch paradygmatów.