Zalety składni od lewej do prawej

18

Oglądałem wywiad z Herbem Sutterem na Channel9 i wspomniał na końcu filmu, że składnia od lewej do prawej będzie na szczycie listy życzeń przyszłego standardu C ++ (chociaż przyznaje, że modyfikuje C ++ w ten sposób stworzyłoby zupełnie inną bestię).

Oprócz:

  • bardziej zrozumiałe dla ludzi, wyraźniejsze gołym okiem; np

    //C syntax
    
    /*pointer to function taking a pointer to function(which takes 2 integers as 
    
    arguments and returns an int), and an int as arguments and returning an int*/
    
    int (*fp)(int (*ff)(int x, int y), int b)
    
    //Go analogous syntax which is left to write
    
    f func(func(int,int) int, int) int
  • łatwiejsze do przeanalizowania (prowadzi do lepszego wsparcia narzędzia, jak wspomniano w filmie - np. refaktoryzacja kodu)

jakie są inne zalety składni „od lewej do prawej” w języku programowania. Wiem tylko, że Pascal i Go stosują taką składnię (i Go nawet nie idzie na całość, jak rozumiem z tego postu na blogu, z którego wziąłem również przykłady) Czy byłoby możliwe posiadanie systemowego języka programowania z tego rodzaju składni?

celavek
źródło
1
haskell używa lewej do prawej:f :: (Int -> Int -> Int) -> Int -> Int
Karoly Horvath,
1
ActionScript, a także: function strlen(s:String):int {...}. Również wpisany rachunek lambda (stąd Haskell).
outis
2
Czy ktoś mógłby wyjaśnić, proszę, głosy końcowe :)? Nie widzę powodu, aby go zamknąć, ale może zadaję „złe” pytanie.
1
Nie głosowałem za zamknięciem, ale komentarz @Devjosh jest trafny, jest bardziej odpowiedni dla programistów, mam nadzieję, że ktoś go migruje ....
Nim
3
@Frank: i nie zapomnij, w przypadku wskaźników funkcji, że składnia jest niezręczna, ponieważ rzeczywisty typ jest podzielony ! To niski cios ...
Matthieu M.,

Odpowiedzi:

12

Podstawową zaletą jest to, że parsowanie jest prostsze i unikalne. Zauważ, że po przeanalizowaniu wiersza kompilator będzie wiedział, jaki jest dokładnie typ, więc odtąd sposób zdefiniowania typu nie ma znaczenia.

Obecnie trudno odczytać dowolną funkcję, która zwraca argument typu tablicowego lub typu wskaźnika funkcji:

// common way of obtaining the static size in elements of an array:
template <typename T, int N>
char (&array_size_impl( T (&)[N] ))[N];
// alternative parser:
template <typename T, int N>               // this would probably be changed also
array_simple_impl function( T [N] & ) char [N] &;

I byłoby mniej szans na nieporozumienie (jako najbardziej dokuczliwa analiza ):

// Current C++
type x( another_type() );      // create an instance x of type passing a
                               // default constructed another_type temporary?
                               // or declare a function x that returns type and takes as argument
                               // a function that has no arguments and returns another_type
// How the compiler reads it:
x function( function() another_type ) type;

// What many people mean:
x type { another_type{} };

Zastosowanie podobnego podejścia do jednolitej inicjalizacji w C ++ 0x (tj. {}Do identyfikacji inicjalizacji). Zauważ, że przy podejściu od lewej do prawej jest znacznie wyraźniejsze to, co definiujemy. Wiele osób (na pewno mnie) ugryzło w dowolnym momencie ten błąd analizowania w przeszłości (więcej niż jeden raz) i nie byłoby tak w przypadku składni od lewej do prawej.

David Rodríguez - dribeas
źródło
Co powiesz na wyrażenia? Jak ta „składnia od lewej do prawej” wpłynie na pierwszeństwo operatora i kolejność oceny?
1
@celavek: Jeśli wrócisz do wywiadu, zauważysz, że nie chce on zmieniać całej składni języka, tylko deklaracje i definicje . Oczywiście, które mogą przenikać inne wyrażenia, nie jestem zbyt pewien, czy ostatni wiersz w powyższych przykładach jest poprawny od lewej do prawej (pomyśl o tym, jak tworzony jest tymczasowy, który może wymagać zmiany ... w C # rozwiązuje się to, podając newoperatorowi dwie semantyki , structlub class, co nie ma zastosowania do C ++, ponieważ w C ++ nie ma rozróżnienia na typy wartości / odniesienia.
David Rodríguez - dribeas
5

Jak się tu dostaliśmy

Składnia C do deklarowania punktów funkcyjnych miała na celu odzwierciedlenie użycia. Rozważ taką deklarację funkcji jak ta <math.h>:

double round(double number);

Aby mieć zmienną punktową, możesz przypisać ją do typu bezpieczeństwa za pomocą

fp = round;

należałoby zadeklarować tę fpzmienną punktową w ten sposób:

double (*fp)(double number);

Wystarczy więc spojrzeć na sposób użycia funkcji i zastąpić nazwę tej funkcji odwołaniem do wskaźnika, przekształcając się roundw *fp. Potrzebny jest jednak dodatkowy zestaw parenów, co według niektórych sprawia, że ​​jest nieco bardziej chaotyczny.

Prawdopodobnie było to łatwiejsze w oryginalnym C, który nawet nie miał podpisu funkcji, ale nie wracajmy tam, ok?

Miejsce, w którym staje się szczególnie nieprzyjemne, zastanawia się, jak zadeklarować funkcję, która albo bierze jako argument, albo zwraca wskaźnik do funkcji, albo jedno i drugie.

Jeśli miałeś funkcję:

void myhandler(int signo);

możesz przekazać go do funkcji sygnału (3) w ten sposób:

signal(SIGHUP, myhandler);

lub jeśli chcesz zachować stary moduł obsługi, to

old_handler = signal(SIGHUP, new_handler);

co jest dość łatwe. To, co jest dość łatwe - ani ładne, ani łatwe - to prawidłowe wypełnianie deklaracji.

signal(int signo, ???)

Cóż, po prostu wróć do deklaracji funkcji i zamień nazwę na odniesienie do punktu:

signal(int sendsig, void (*hisfunc)(int gotsig));

Ponieważ nie deklarujesz gotsig, możesz łatwiej przeczytać, jeśli pominiesz:

signal(int sendsig, void (*hisfunc)(int));

Albo może nie. :(

Tyle że nie jest to wystarczająco dobre, ponieważ signal (3) zwraca również stary moduł obsługi, jak w:

old_handler = signal(SIGHUP, new_handler);

Więc teraz musisz dowiedzieć się, jak je wszystkie zadeklarować.

void (*old_handler)(int gotsig);

wystarczy dla zmiennej, do której zamierzasz przypisać. Pamiętaj, że tak naprawdę nie deklarujesz gotsigtutaj old_handler. To naprawdę wystarczy:

void (*old_handler)(int);

To prowadzi nas do poprawnej definicji sygnału (3):

void (*signal(int signo, void (*handler)(int)))(int);

Typedefs na ratunek

Myślę, że do tego czasu wszyscy zgodzą się, że to bałagan. Czasami lepiej nazwać swoje abstrakcje; często naprawdę. Dzięki prawu typedefstaje się to znacznie łatwiejsze do zrozumienia:

typedef void (*sig_t) (int);

Teraz twoja własna zmienna modułu obsługi staje się

sig_t old_handler, new_handler;

a twoja deklaracja sygnału (3) staje się słuszna

sig_t signal(int signo, sig_t handler);

co jest nagle zrozumiałe. Pozbycie się znaku * pozbywa się także mylących nawiasów (i mówią, że pareny zawsze ułatwiają zrozumienie - hah!). Twoje użycie jest nadal takie samo:

old_handler = signal(SIGHUP, new_handler);

ale teraz masz szansę zrozumienia dla deklaracji old_handler, new_handlera nawet signalkiedy po raz pierwszy je lub potrzebę spotkać je pisać.

Wniosek

Okazuje się, że bardzo niewielu programistów C jest w stanie samodzielnie opracować prawidłowe deklaracje dla tych rzeczy bez konsultacji z materiałami referencyjnymi.

Wiem, ponieważ kiedyś mieliśmy to pytanie w naszych rozmowach kwalifikacyjnych dla osób wykonujących pracę jądra i sterownika urządzenia. :) Jasne, w ten sposób straciliśmy wielu kandydatów, którzy rozbili się i spalili na tablicy. Ale unikaliśmy również zatrudniania osób, które twierdziły, że mają wcześniejsze doświadczenie w tej dziedzinie, ale nie mogły właściwie wykonać pracy.

Ze względu na tę powszechną trudność, prawdopodobnie nie jest rozsądne, ale wręcz rozsądne, aby mieć sposób na spełnienie wszystkich tych deklaracji, które nie wymagają już od ciebie bycia programistą potrójnego alfy, siedzącym trzy sigmy powyżej średniej tylko po to, aby tego użyć. coś w rodzaju komfortu.

tchrist
źródło
1
Zaprojektowany, trudny do naśladowania ... +1 za wysiłek, ponieważ ilustruje to, że czasami trudno jest to zrobić poprawnie w C.
celavek
4

Myślę, że nieco straciłeś punkt, kiedy skupiłeś się na bitach od lewej do prawej.

Problemami w C i C ++ są horrendalna gramatyka, którą mają, która jest trudna zarówno do odczytania (ludzie), jak i do analizy (narzędzia).

Bardziej spójna (lub regularna ) gramatyka ułatwia obie te rzeczy. A łatwiejsze parsowanie oznacza łatwiejsze oprzyrządowanie: większość obecnych narzędzi nie ma poprawnego C ++, nawet najnowszej wtyczki Eclipse, gdy próbowali odkryć koło ... i zawiodły, i prawdopodobnie mają więcej osób niż przeciętny projekt systemu operacyjnego.

Prawdopodobnie więc przybiłeś ją, koncentrując się na czytaniu i analizie ... i to wielka sprawa :)

Matthieu M.
źródło
To wielka niespodzianka, że ​​Eclipse wciąż nie jest w stanie przeanalizować takich rzeczy jak powyższe deklaracje. Dlaczego nie używają prawdziwego parsera C jak z gcc?
tchrist
@tchrist: Wykryłem dwa problemy z Eclipse i oba wydają się być powiązane z makrami. Być może bardziej dotyczy to preprocesora niż generacji AST.
Matthieu M.,