Zastanawiałem się, dlaczego istnieją (we wszystkich językach programowania, których się nauczyłem, takich jak C ++, Java, Python) standardowe biblioteki, takie jak stdlib, zamiast mieć podobne „funkcje” będące prymitywem samego języka.
Zastanawiałem się, dlaczego istnieją (we wszystkich językach programowania, których się nauczyłem, takich jak C ++, Java, Python) standardowe biblioteki, takie jak stdlib, zamiast mieć podobne „funkcje” będące prymitywem samego języka.
Odpowiedzi:
Pozwólcie mi rozwinąć nieco dobrą odpowiedź @ Vincenta (+1) :
Dlaczego kompilator nie mógł po prostu przetłumaczyć wywołania funkcji na zestaw instrukcji?
Może i robi to za pomocą co najmniej dwóch mechanizmów:
wstawianie wywołania funkcji - podczas tłumaczenia kompilator może zastąpić wywołanie kodu źródłowego z implementacją bezpośrednio wbudowaną, zamiast rzeczywistego wywołania funkcji. Mimo to funkcja musi mieć gdzieś zdefiniowaną implementację, która może znajdować się w standardowej bibliotece.
funkcja wewnętrzna - funkcja wewnętrzna to funkcje, o których poinformowano kompilator, niekoniecznie znajdując funkcję w bibliotece. Zazwyczaj są one zarezerwowane dla funkcji sprzętowych, które nie są praktycznie dostępne w żaden inny sposób, ponieważ są tak proste, że nawet narzut wywołania funkcji biblioteki języka asemblera jest uważany za wysoki. (Kompilator może generalnie tylko automatycznie wstawiać kod źródłowy w swoim języku, ale nie funkcje asemblera, czyli tam, gdzie wchodzi wewnętrzny mechanizm.)
Mimo to, najlepszą opcją jest czasami przetłumaczenie przez kompilator wywołania funkcji w języku źródłowym na wywołanie funkcji w kodzie maszynowym. Rekurencja, metody wirtualne i sama wielkość to niektóre powody, dla których wstawianie nie zawsze jest możliwe / praktyczne. (Innym powodem jest zamiar kompilacji, taki jak osobna kompilacja (moduły obiektowe), osobne jednostki ładujące (np. DLL)).
Nie ma też żadnej realnej przewagi, by większość standardowych funkcji bibliotecznych była intriskami (co znacznie utrudniłoby kompilację wiedzy w kompilatorze, bez żadnej realnej korzyści), dlatego często najbardziej odpowiednie jest ponowne wywołanie kodu maszynowego.
C jest znaczącym językiem, który prawdopodobnie pomija inne jawne instrukcje językowe na korzyść standardowych funkcji bibliotecznych. Choć biblioteki istniały wcześniej, język ten przeszedł do wykonywania większej ilości pracy od standardowych funkcji bibliotecznych, a mniej jako wyraźnych instrukcji w gramatyce języka. Na przykład IO w innych językach często otrzymywało własną składnię w postaci różnych instrukcji, podczas gdy gramatyka C nie definiuje żadnych instrukcji IO, po prostu odkłada na swoją standardową bibliotekę, aby zapewnić, że wszystkie są dostępne poprzez wywołania funkcji, które kompilator już wie, jak to zrobić.
Ma to na celu uproszczenie samego języka. Musisz rozróżnić funkcję języka, taką jak rodzaj pętli lub sposoby przekazywania parametrów do funkcji itd., Oraz wspólną funkcjonalność, której potrzebuje większość aplikacji.
Biblioteki to funkcje, które mogą być przydatne dla wielu programistów, dlatego są tworzone jako kod wielokrotnego użytku, który można udostępniać. Standardowe biblioteki są zaprojektowane tak, aby były bardzo powszechnymi funkcjami, których zwykle potrzebują programiści. W ten sposób język programowania jest natychmiast przydatny dla szerszego grona programistów. Biblioteki można aktualizować i rozszerzać bez zmiany podstawowych funkcji samego języka.
PHP
jako przykład prawie nie robi różnic między jego rozległymi funkcjami językowymi a samym językiem.
include
, require
i require_once
, jeśli / do / while (programowania strukturalnego), wyjątkami, oddzielny system „wartości błędu”, skomplikowane słabe zasady typowania, skomplikowane zasady pierwszeństwa operator, i tak dalej . Porównaj to z prostotą, powiedzmy, Smalltalk, Scheme, Prolog, Forth itp.;)
Oprócz tego, co już powiedzieli inne odpowiedzi, umieszczenie standardowych funkcji w bibliotece to oddzielenie problemów :
Kompilatorem jest parsowanie języka i generowanie dla niego kodu. Kompilator nie musi zawierać niczego, co można już napisać w tym języku i udostępnić jako bibliotekę.
Jest to zadanie biblioteki standardowej (zawsze domyślnie dostępnej) zapewniające podstawową funkcjonalność, która jest potrzebna praktycznie we wszystkich programach. Zawieranie wszystkich funkcji, które mogą być przydatne, nie jest zadaniem standardowej biblioteki.
Zadaniem opcjonalnych bibliotek standardowych jest zapewnienie funkcji pomocniczych, bez których wiele programów może się obejść, ale które są nadal dość podstawowe i również niezbędne w wielu aplikacjach, aby zagwarantować wysyłkę w standardowych środowiskach. Nie jest zadaniem tych opcjonalnych bibliotek przechowywanie całego kodu wielokrotnego użytku, który kiedykolwiek został napisany.
Biblioteki użytkowników mają za zadanie udostępniać kolekcje przydatnych funkcji wielokrotnego użytku. Nie jest zadaniem bibliotek użytkownika przechowywanie całego kodu, który kiedykolwiek został napisany.
Zadaniem kodu źródłowego aplikacji jest dostarczenie pozostałych fragmentów kodu, które tak naprawdę dotyczą tylko tej jednej aplikacji.
Jeśli potrzebujesz uniwersalnego oprogramowania, otrzymasz coś niesamowicie złożonego. Musisz zmodularyzować, aby zredukować złożoność do możliwych do zarządzania poziomów. Musisz zmodularyzować, aby umożliwić częściowe wdrożenia:
Biblioteka wątków jest bezwartościowa na wbudowanym kontrolerze jednordzeniowym. Umożliwienie implementacji języka dla tego wbudowanego kontrolera po prostu nie dołączania pthread
biblioteki jest właściwym rozwiązaniem.
Biblioteka matematyczna jest bezwartościowa na mikrokontrolerze, który nawet nie ma FPU. Ponownie, nie będąc zmuszonym do dostarczania funkcji takich jak sin()
życie, jest znacznie łatwiejsze dla implementatorów twojego języka dla tego mikrokontrolera.
Nawet podstawowa biblioteka standardowa jest bezwartościowa, gdy programujesz jądro. Nie możesz zaimplementować write()
bez syscall w jądrze i nie możesz zaimplementować printf()
bez write()
. Jako programista jądra Twoim zadaniem jest zapewnić write()
wywołanie systemowe, nie możesz po prostu oczekiwać, że będzie ono dostępne.
Język, który nie pozwala na takie pominięcia w standardowych bibliotekach, po prostu nie nadaje się do wielu zadań . Jeśli chcesz, aby Twój język był elastycznie używany w nietypowych środowiskach, musi on być elastyczny w zakresie bibliotek standardowych. Im bardziej twój język opiera się na bibliotekach standardowych, tym więcej przyjmuje założeń dotyczących środowiska wykonawczego, a tym samym ogranicza jego użycie do środowisk, które zapewniają te warunki wstępne.
Oczywiście języki wysokiego poziomu, takie jak Python i Java, mogą przyjmować wiele założeń dotyczących ich środowiska. I zwykle zawierają wiele, wiele rzeczy w swoich standardowych bibliotekach. Języki niższego poziomu, takie jak C, zapewniają znacznie mniej w swoich standardowych bibliotekach i znacznie zmniejszają podstawową bibliotekę standardową. Dlatego znajdujesz działający kompilator C dla praktycznie dowolnej architektury, ale może nie być w stanie uruchomić na nim żadnych skryptów Pythona.
Jednym z głównych powodów, dla których kompilatory i biblioteki standardowe są oddzielne, jest to, że służą one dwóm różnym celom (nawet jeśli oba są zdefiniowane przez tę samą specyfikację językową): kompilator tłumaczy kod wyższego poziomu na instrukcje maszynowe, a biblioteka standardowa zapewnia wstępnie przetestowane wdrożenia powszechnie potrzebnej funkcjonalności. Autorzy kompilatorów cenią modułowość, podobnie jak inni twórcy oprogramowania. W rzeczywistości niektóre wczesne kompilatory C dalej dzielą kompilator na osobne programy do wstępnego przetwarzania, kompilacji i łączenia.
Ta modułowość daje wiele zalet:
Historycznie rzecz biorąc (przynajmniej z perspektywy C) oryginalne, przednormalizacyjne wersje języka w ogóle nie miały standardowej biblioteki. Dostawcy systemów operacyjnych i firmy zewnętrzne często zapewniały biblioteki pełne często używanych funkcji, ale różne implementacje obejmowały różne rzeczy i były w dużej mierze niekompatybilne ze sobą. Kiedy C zostało znormalizowane, zdefiniowano „standardową bibliotekę”, próbując zharmonizować te odmienne implementacje i poprawić przenośność. Standardowa biblioteka C opracowana oddzielnie od języka, podobnie jak biblioteki Boost dla C ++, ale później zostały zintegrowane ze specyfikacją języka.
Dodatkowa odpowiedź narożna: zarządzanie własnością intelektualną
Godnym uwagi przykładem jest implementacja Math.Pow (double, double) w .NET Framework, który został zakupiony przez Microsoft od Intela i pozostaje nieujawniony, nawet jeśli framework został otwarty. (Mówiąc ściślej, w powyższym przypadku jest to wywołanie wewnętrzne, a nie biblioteka, ale idea się utrzymuje). Biblioteka oddzielona od samego języka (teoretycznie również podzbiór bibliotek standardowych) może dać osobom popierającym język większą elastyczność w rysowaniu granica między tym, co należy zachować przejrzystość, a tym, co musi pozostać niejawne (ze względu na ich umowy ze stronami trzecimi lub inne powody związane z własnością intelektualną).
Math.Pow
, nie wspomina o żadnym zakupie ani o Intelie i mówi o ludziach czytających kod źródłowy implementacji funkcji.
Błędy i debugowanie.
Błędy: całe oprogramowanie zawiera błędy, w standardowej bibliotece występują błędy, a na kompilatorze występują błędy. Jako użytkownik języka znacznie łatwiej jest znaleźć i obejść takie błędy, gdy znajdują się one w standardowej bibliotece, a nie w kompilatorze.
Debugowanie: Znacznie łatwiej jest mi zobaczyć ślad stosu standardowej biblioteki i dać mi poczucie, co może pójść nie tak. Ponieważ ten ślad stosu zawiera kod, który rozumiem. Oczywiście możesz głębiej kopać, a także śledzić swoje wewnętrzne funkcje, ale jest to o wiele łatwiejsze, jeśli używasz języka, którego używasz przez cały dzień.
To doskonałe pytanie!
Na przykład standard C ++ nigdy nie określa, co powinno zostać zaimplementowane w kompilatorze lub w standardowej bibliotece: odnosi się tylko do implementacji . Na przykład symbole zastrzeżone są definiowane zamiennie zarówno przez kompilator (jako elementy wewnętrzne), jak i przez bibliotekę standardową.
Jednak wszystkie implementacje C ++, o których wiem, będą miały minimalną możliwą liczbę elementów wewnętrznych dostarczonych przez kompilator i możliwie jak najwięcej ze standardowej biblioteki.
Zatem, chociaż technicznie wykonalne jest zdefiniowanie biblioteki standardowej jako funkcji wewnętrznej w kompilatorze, wydaje się, że jest rzadko stosowane w praktyce.
Rozważmy pomysł przeniesienia części funkcjonalności ze standardowej biblioteki do kompilatora.
Zalety:
Niedogodności:
std
) do eksperymentowania.Oznacza to, że przeniesienie czegoś do kompilatora jest drogie , teraz iw przyszłości, a zatem wymaga solidnego uzasadnienia. W przypadku niektórych funkcji jest to konieczne (nie można ich zapisać jako zwykłego kodu), ale nawet wtedy opłaca się wyodrębnić minimalne i ogólne elementy, aby przejść do kompilatora i zbudować je na górze w standardowej bibliotece.
Jako projektant języka chciałbym powtórzyć niektóre inne odpowiedzi tutaj, ale podaj je oczami kogoś, kto buduje język.
Interfejs API nie jest gotowy, gdy skończysz dodawać do niego wszystko, co możesz. Interfejs API jest gotowy, gdy skończysz wyciągać z niego wszystko, co możesz.
Język programowania należy określić za pomocą jakiegoś języka. Musisz umieć przekazać znaczenie każdego programu napisanego w twoim języku. Ten język jest bardzo trudny do napisania, a jeszcze trudniejszy do dobrego pisania. Ogólnie rzecz biorąc, jest to bardzo precyzyjna i dobrze zorganizowana forma języka angielskiego używana do przekazywania znaczenia nie komputerowi, ale innym programistom, zwłaszcza tym, którzy piszą kompilatory lub tłumacze dla twojego języka. Oto przykład ze specyfikacji C ++ 11, [intro.multithread / 14]:
Widoczna sekwencja efektów ubocznych na obiekcie atomowym M, w odniesieniu do obliczenia wartości B M, jest maksymalną ciągłą podsekwencją efektów ubocznych w kolejności modyfikacji M, gdzie pierwszy efekt uboczny jest widoczny w odniesieniu do B , a dla każdego efektu ubocznego B nie dzieje się przed nim. Wartość obiektu atomowego M, ustalona na podstawie oceny B, będzie wartością zapisaną przez jakąś operację w widocznej sekwencji M względem B. [Uwaga: Można wykazać, że widoczna sekwencja efektów ubocznych wartości obliczenia są unikalne, biorąc pod uwagę poniższe wymagania dotyczące spójności. —Wskazówka]
Blek! Każdy, kto pogrążył się w zrozumieniu, w jaki sposób C ++ 11 obsługuje wielowątkowość, może docenić, dlaczego to sformułowanie musi być tak nieprzejrzyste, ale to nie wybacza faktu, że jest ... cóż ... tak nieprzejrzysty!
Porównaj to z definicją std::shared_ptr<T>::reset
w sekcji biblioteki normy:
template <class Y> void reset(Y* p);
Efekty: równoważne z
shared_ptr(p).swap(*this)
Jaka jest różnica? W części dotyczącej definicji języka autorzy nie mogą zakładać, że czytelnik rozumie prymitywy językowe. Wszystko musi być dokładnie określone w angielskiej prozie. Po przejściu do części dotyczącej definicji biblioteki możemy użyć języka do określenia zachowania. Często jest to o wiele łatwiejsze!
Zasadniczo można mieć płynną rozbudowę z prymitywów na początku dokumentu specyfikacji, aż do zdefiniowania, co uważamy za „standardowe funkcje biblioteki”, bez konieczności rysowania linii między „prymitywami językowymi” a funkcje „standardowej biblioteki”. W praktyce ta linia okazuje się niezwykle cenna do rysowania, ponieważ pozwala pisać niektóre z najbardziej skomplikowanych części języka (takie jak te, które muszą implementować algorytmy) przy użyciu języka zaprojektowanego do ich wyrażania.
I rzeczywiście widzimy pewne rozmyte linie:
java.lang.ref.Reference<T>
mogą być podklasowane tylko przez standardowe klasy bibliotek, java.lang.ref.WeakReference<T>
java.lang.ref.SoftReference<T>
a java.lang.ref.PhantomReference<T>
ich zachowanie Reference
jest tak głęboko powiązane ze specyfikacją języka Java, że musieli wprowadzić pewne ograniczenia w części tego procesu zaimplementowanej jako klasy „biblioteki standardowej”.Ma to stanowić uzupełnienie istniejących odpowiedzi (i jest zbyt długie, aby można było dodać komentarz).
Istnieją co najmniej dwa inne powody dla standardowej biblioteki:
Jeśli dana funkcja językowa jest w funkcji biblioteki i chcę wiedzieć, jak to działa, mogę po prostu odczytać źródło tej funkcji. Jeśli chcę przesłać zgłoszenie błędu / prośbę o łatkę / ściągnięcie, kodowanie poprawki i przypadków testowych nie jest na ogół zbyt trudne. Jeśli jest w kompilatorze, muszę być w stanie zagłębić się w wewnętrzne elementy. Nawet jeśli jest w tym samym języku (i tak powinno być, każdy szanujący się kompilator powinien być hostem) kod kompilatora nie przypomina kodu aplikacji. Znalezienie odpowiednich plików może potrwać wieczność.
Odcinasz się od wielu potencjalnych współpracowników, jeśli pójdziesz tą drogą.
Wiele języków oferuje tę funkcję w takim czy innym stopniu, ale ogromnie skomplikowane byłoby przeładowanie na gorąco kodu wykonującego przeładowanie na gorąco. Jeśli SL jest oddzielny od środowiska wykonawczego, można go ponownie załadować.
To interesujące pytanie, ale jest już wiele dobrych odpowiedzi, więc nie podejmę się pełnego.
Jednak dwie rzeczy, które moim zdaniem nie przyciągnęły wystarczającej uwagi:
Po pierwsze, całość nie jest super wyraźna. To trochę spektrum właśnie dlatego, że istnieją powody, aby robić różne rzeczy. Na przykład kompilatory często wiedzą o standardowych bibliotekach i ich funkcjach. Przykład przykładu: funkcja „Hello World” C - printf - jest najlepsza, jaką mogę wymyślić. Jest to funkcja biblioteczna, jakoś musi być, ponieważ jest bardzo zależna od platformy. Ale jego zachowanie (zdefiniowane w implementacji) musi być znane kompilatorowi, aby ostrzec programistę przed złymi wywołaniami. Nie jest to szczególnie miłe, ale było postrzegane jako dobry kompromis. Nawiasem mówiąc, jest to prawdziwa odpowiedź na większość pytań „dlaczego ten projekt”: dużo kompromisu i „wydawało się wtedy dobrym pomysłem”. Nie zawsze „był to dobry sposób na zrobienie tego” lub „
Po drugie, pozwala standardowej bibliotece nie być tym standardem. Istnieje wiele sytuacji, w których język jest pożądany, ale standardowe biblioteki, które zwykle mu towarzyszą, nie są zarówno praktyczne, jak i pożądane. Najczęściej dzieje się tak w przypadku systemów programowania języków, takich jak C, na niestandardowych platformach. Na przykład, jeśli masz system bez systemu operacyjnego lub programu planującego: nie będziesz mieć wątków.
W przypadku standardowego modelu biblioteki (i obsługiwanego w nim wątkowania) można to obsłużyć czysto: kompilator jest prawie taki sam, można ponownie użyć bitów bibliotek, które mają zastosowanie, i wszystkiego, czego nie można usunąć. Jeśli zostanie to wstawione do kompilatora, sprawy zaczną się nieporządnie.
Na przykład:
Nie możesz być kompatybilnym kompilatorem.
Jak wskazałbyś swoje odchylenie od normy? Zauważ, że zwykle istnieje jakaś forma składni importu / dołączenia, która może nie działać, tj. Import pytonów lub dołączenie C, które łatwo wskazują na problem, jeśli brakuje czegoś w standardowym modelu biblioteki.
Podobne problemy mają również zastosowanie, jeśli chcesz ulepszyć lub rozszerzyć funkcjonalność „biblioteki”. Jest to o wiele bardziej powszechne, niż mogłoby się wydawać. Wystarczy trzymać się wątków: Windows, Linux i niektóre egzotyczne jednostki przetwarzające sieć robią wątki zupełnie inaczej. Podczas gdy bity linux / windows mogą być dość statyczne i być w stanie używać identycznego API, rzeczy NPU zmienią się z dniem tygodnia i API z nim. Kompilatory szybko się zmieniają, gdy ludzie decydują, które bity potrzebują do wsparcia / mogą zrobić bez dość szybko, jeśli nie ma sposobu na rozdzielenie tego rodzaju rzeczy.