Próbowałem wyjaśnić komuś, że C jest kompletne w Turinga, i zdałem sobie sprawę, że tak naprawdę nie wiem, czy w rzeczywistości jest to kompletny Turing. (C jak w semantyce abstrakcyjnej, a nie jak w rzeczywistej implementacji).
„Oczywista” odpowiedź (z grubsza: może zająć się dowolną ilością pamięci, więc może emulować maszynę RAM, więc jest kompletna z Turinga), w rzeczywistości, o ile wiem, nie jest poprawna, ponieważ chociaż standard C pozwala aby size_t był arbitralnie duży, musi być ustalony na pewną długość i bez względu na to, na jakiej długości jest ustalony, jest wciąż skończony. (Innymi słowy, chociaż można, biorąc pod uwagę dowolne zatrzymanie maszyny Turinga, wybrać długość size_t tak, aby działała ona „prawidłowo”, nie ma sposobu, aby wybrać długość size_t, tak aby wszystkie maszyny zatrzymujące Turinga działały poprawnie)
Więc: czy C99 Turing jest kompletny?
Odpowiedzi:
Nie jestem pewien, ale myślę, że odpowiedź brzmi „nie” z dość subtelnych powodów. I poprosił o Theoretical Computer Science kilka lat temu i nie uzyskać odpowiedź, która wykracza poza to, co ja tu obecnych.
W większości języków programowania można symulować maszynę Turinga poprzez:
W konkretnej implementacji działającej na komputerze zabrakłoby pamięci, gdyby taśma była zbyt długa, ale idealna implementacja mogłaby wiernie wykonać program maszyny Turinga. Można to zrobić za pomocą długopisu i papieru lub kupując komputer z większą pamięcią i kompilatorem ukierunkowanym na architekturę z większą liczbą bitów na słowo itd., Jeśli programowi zabraknie pamięci.
To nie działa w C, ponieważ nie można mieć połączonej listy, która mogłaby rosnąć wiecznie: zawsze istnieje pewne ograniczenie liczby węzłów.
Aby wyjaśnić, dlaczego, najpierw muszę wyjaśnić, czym jest implementacja C. C to właściwie rodzina języków programowania. Standard ISO C (dokładniej, specyficzna wersja tego standardu) określa (z poziomu formalizmu, który pozwala angielska) składni i semantyki to rodzina języków programowania. C ma wiele niezdefiniowanych zachowań i zachowań zdefiniowanych w implementacji. „Implementacja” C kodyfikuje wszystkie zachowania zdefiniowane w implementacji (lista rzeczy do skodyfikowania znajduje się w dodatku J dla C99). Każda implementacja C jest oddzielnym językiem programowania. Zauważ, że znaczenie słowa „implementacja” jest nieco dziwne: tak naprawdę oznacza wariant językowy, może istnieć wiele różnych programów kompilujących, które implementują ten sam wariant językowy.
t
Wartości
CHAR_BIT
isizeof(void*)
są obserwowalne, więc jeśli zabraknie pamięci, nie możesz po prostu wznowić działania programu z większymi wartościami dla tych parametrów. Uruchomiłbyś program w innym języku programowania - innej implementacji C.C nie nakłada bezpośrednio maksymalnej głębokości rekurencji. Implementacja może mieć maksimum, ale także może nie mieć takiego. Ale w jaki sposób komunikujemy się między wywołaniem funkcji a jego rodzicem? Argumenty nie są dobre, jeśli można je adresować, ponieważ pośrednio ograniczyłoby to głębokość rekurencji: jeśli masz funkcję,
int f(int x) { … f(…) …}
wszystkie wystąpieniax
aktywnych ramekf
mają swój własny adres, a więc liczba zagnieżdżonych wywołań jest ograniczona liczbą możliwych adresów dlax
.Program AC może używać pamięci nieadresowalnej w postaci
register
zmiennych. „Normalne” implementacje mogą mieć tylko niewielką, skończoną liczbę zmiennych, które nie mają adresu, ale teoretycznie implementacja może pozwolić na nieograniczoną ilośćregister
pamięci. W takiej implementacji można wykonać nieograniczoną liczbę rekurencyjnych wywołań funkcji, o ile są to jej argumentyregister
. Ale ponieważ argumentami sąregister
, nie możesz zrobić dla nich wskaźnika, więc musisz jawnie skopiować ich dane: możesz przekazać tylko skończoną ilość danych, a nie strukturę danych o dowolnej wielkości złożoną ze wskaźników.Dzięki nieograniczonej głębokości rekurencji i ograniczeniu, że funkcja może tylko pobierać dane z bezpośredniego wywołującego (
register
argumenty) i zwracać dane do bezpośredniego wywołującego (wartość zwracana przez funkcję), uzyskujesz moc deterministycznych automatów do przekazywania .Nie mogę znaleźć sposobu, aby pójść dalej.
(Oczywiście można zmusić program do przechowywania zawartości taśmy na zewnątrz za pomocą funkcji wejścia / wyjścia pliku. Ale wtedy nie zapytałbyś, czy C jest kompletny w Turingu, ale czy C plus nieskończony system pamięci jest kompletny w Turingu, aby na które odpowiedź brzmi nudno „tak”. Równie dobrze możesz zdefiniować pamięć jako wezwanie Turinga
fopen("oracle", "r+")
,fwrite
początkową zawartość taśmy ifread
cofnąć końcową zawartość taśmy).źródło
Dodanie przez C99
va_copy
argumentu variadic API może dać nam tylne drzwi do kompletności Turinga. Ponieważ staje się możliwe wielokrotne iterowanie poprzez listę argumentów variadic więcej niż raz w funkcji innej niż ta, która pierwotnie otrzymała argumenty,va_args
można zastosować do implementacji wskaźnika bez wskaźnika.Oczywiście prawdziwa implementacja interfejsu API argumentu variadic prawdopodobnie będzie miała gdzieś wskaźnik, ale w naszej abstrakcyjnej maszynie może być zaimplementowana za pomocą magii.
Oto wersja demonstracyjna implementująca automat na 2 stosy z dowolnymi regułami przejścia:
Uwaga: Jeśli
va_list
jest to typ tablicowy, to w rzeczywistości istnieją ukryte parametry wskaźnika do funkcji. Więc prawdopodobnie lepiej byłoby zmienić typy wszystkichva_list
argumentów nawrapped_stack
.źródło
va_list
zmiennych automatycznychstack
. Te zmienne muszą mieć adres&stack
, a my możemy mieć tylko ich ograniczoną liczbę. Może ten wymóg można obejść, deklarując każdą zmienną lokalnąregister
?register
.int
być wymagane, aby mieć granicę, chyba że ktoś użyje tej granicy lubsizeof(int)
?int
ma wartość między niektórymi skończonymi granicamiINT_MIN
aINT_MAX
. A jeśli wartośćint
przelewu przekroczy te związane, zachodzi niezdefiniowane zachowanie. Z drugiej strony standard celowo nie wymaga, aby wszystkie obiekty były fizycznie obecne w pamięci pod określonym adresem, ponieważ pozwala to na optymalizacje, takie jak przechowywanie obiektu w rejestrze, przechowywanie tylko części obiektu, reprezentując go inaczej niż standard układ lub całkowicie go pominąć, jeśli nie jest potrzebny.Może niestandardowa arytmetyka?
Wygląda więc na to, że problemem jest skończona wielkość
sizeof(t)
. Myślę jednak, że znam rozwiązanie.O ile mi wiadomo, C nie wymaga implementacji, aby używać standardowych liczb całkowitych dla swojego typu liczb całkowitych. Dlatego moglibyśmy zastosować niestandardowy model arytmetyczny . Wówczas ustalilibyśmy
sizeof(t)
jakąś niestandardową liczbę, a teraz nigdy nie osiągniemy jej w skończonej liczbie kroków. Dlatego długość taśmy maszyn Turinga zawsze będzie mniejsza niż „maksimum”, ponieważ maksimum jest dosłownie niemożliwe do osiągnięcia.sizeof(t)
po prostu nie jest liczbą w zwykłym tego słowa znaczeniu.Jest to oczywiście jedna technika: twierdzenie Tennenbauma . Stwierdza, że jedynym modelem arytmetyki Peano jest model standardowy, który oczywiście nie zrobiłby tego. Jednak, o ile mi wiadomo, C nie wymaga implementacji do używania typów danych, które spełniają aksjomaty Peano, ani nie wymaga implementacji do obliczenia, więc nie powinno to stanowić problemu.
Co powinno się stać, jeśli spróbujesz wypisać niestandardową liczbę całkowitą? Cóż, możesz reprezentować dowolną niestandardową liczbę całkowitą za pomocą niestandardowego ciągu, więc po prostu przesyłaj cyfry z przodu tego ciągu.
źródło
sizeof(t)
jest wartością typusize_t
, więc jest naturalną liczbą całkowitą z zakresu od 0 doSIZE_MAX
.IMO, silnym ograniczeniem jest to, że przestrzeń adresowalna (poprzez rozmiar wskaźnika) jest skończona i nie można jej odzyskać.
Można powiedzieć, że pamięć można „zamienić na dysk”, ale w pewnym momencie sama informacja o adresie przekroczy rozmiar adresowalny.
źródło
W praktyce ograniczenia te nie mają znaczenia dla kompletności Turinga. Rzeczywistym wymogiem jest, aby taśma była dowolna długa, a nie nieskończona. To stworzyłoby problem zatrzymania innego rodzaju (jak wszechświat „oblicza” taśmę?)
Jest to tak nieprawdziwe, jak powiedzenie „Python nie jest kompletny, ponieważ nie można zrobić nieskończenie dużej listy”.
[Edycja: dzięki Panu Whitledgeowi za wyjaśnienie, jak edytować.]
źródło
size_t
jest skończona. Problem polega na tym, że nie można ustalić powiązania,size_t
które jest ważne podczas obliczeń: dla dowolnego powiązania program może go przepełnić. Ale język C stwierdza, że istnieje granicasize_t
: w danej implementacji może ona rosnąć tylko dosizeof(size_t)
bajtów. Ponadto, byłoby miło . Mówienie, że ludzie, którzy cię krytykują „nie mogą myśleć samodzielnie”, jest niegrzeczne.Nośniki wymienne pozwalają nam ominąć problem nieograniczonej pamięci. Być może ludzie pomyślą, że to nadużycie, ale myślę, że jest to w porządku i zasadniczo nieuniknione.
Napraw dowolne wdrożenie uniwersalnej maszyny Turinga. Do taśmy używamy nośników wymiennych. Gdy głowica opuści koniec lub początek bieżącej płyty, urządzenie wyświetli monit o włożenie następnego lub poprzedniego. Możemy użyć specjalnego znacznika do oznaczenia lewego końca symulowanej taśmy lub mieć taśmę, która jest nieograniczona w obu kierunkach.
Kluczową kwestią jest to, że wszystko, co program C musi zrobić, jest skończone. Komputer potrzebuje tylko wystarczającej ilości pamięci, aby zasymulować automat, i
size_t
musi być wystarczająco duży, aby umożliwić adresowanie tej (właściwie raczej małej) ilości pamięci i na dyskach, które mogą mieć dowolny ustalony rozmiar. Ponieważ użytkownik jest monitowany tylko o włożenie następnej lub poprzedniej płyty, nie potrzebujemy bezgranicznie dużych liczb całkowitych, aby powiedzieć „Proszę włóż dysk o numerze 123456 ...”Przypuszczam, że głównym zarzutem jest prawdopodobnie zaangażowanie użytkownika, ale wydaje się to nieuniknione w żadnej implementacji, ponieważ wydaje się, że nie ma innego sposobu implementacji nieograniczonej pamięci.
źródło
Wybierz
size_t
być nieskończenie dużyMożesz wybrać
size_t
nieskończenie duży rozmiar. Oczywiście nie można zrealizować takiego wdrożenia. Ale to nie jest niespodzianka, biorąc pod uwagę skończoną naturę świata, w którym żyjemy.Praktyczne implikacje
Ale nawet gdyby możliwe było zrealizowanie takiego wdrożenia, pojawiałyby się problemy praktyczne. Rozważ następujące oświadczenie C:
SIZE_MAX
SIZE_MAX
size_t
SIZE_MAX
printf
Na szczęście dla naszych teoretycznych celów nie znalazłem w specyfikacji żadnego wymagania, które gwarantowałoby, że gwarancje
printf
wygasną dla wszystkich danych wejściowych. Tak więc, o ile mi wiadomo, nie naruszamy tutaj specyfikacji C.O kompletności obliczeniowej
Pozostaje jeszcze udowodnić, że nasza teoretyczna implementacja jest zakończona w Turinga . Możemy to pokazać, wdrażając „dowolną maszynę Turinga z pojedynczą taśmą”.
Większość z nas prawdopodobnie wdrożyła maszynę Turinga jako projekt szkolny. Nie podam szczegółów konkretnej implementacji, ale oto często stosowana strategia:
Zobaczmy teraz, co jest wymagane do realizacji takiej implementacji:
MAX_INT
również nieskończoność. (Alternatywnie, moglibyśmy użyć innych obiektów do przedstawienia stanów i symboli.)malloc
, ale jest jeszcze coś, co musimy wziąć pod uwagę:malloc
na awarię, jeśli np. Dostępna pamięć zostanie wyczerpana. Dlatego nasze wdrożenie jest naprawdę uniwersalne, jeślimalloc
nigdy nie zawiedzie.malloc
aby zawieść. Bez naruszenia standardu C nasza implementacja zagwarantuje, żemalloc
nigdy nie zawiedzie.Tak więc powyższa lista jest niezbędna do wdrożenia maszyny Turinga w naszej hipotetycznej implementacji C. Funkcje te muszą zostać zakończone. Cokolwiek innego może jednak nie zostać wypowiedziane (chyba że wymaga tego standard). Obejmuje to arytmetykę, operacje we / wy itp.
źródło
printf("%zu\n",SIZE_MAX);
wydrukuje na takiej implementacji?sizeof(size_t)
(lubCHAR_BITS
). Nie możesz wznowić od nowego stanu, musisz zacząć od nowa, ale wykonanie programu może być inne teraz, gdy te stałe są różneGłównym argumentem tutaj było to, że rozmiar size_t jest skończony, chociaż może być nieskończenie duży.
Istnieje obejście tego problemu, choć nie jestem pewien, czy jest to zgodne z ISO C.
Załóżmy, że masz maszynę z nieskończoną pamięcią. Zatem nie jesteś ograniczony wielkością wskaźnika. Nadal masz typ size_t. Jeśli zapytasz mnie, co to jest sizeof (size_t), odpowiedzią będzie tylko sizeof (size_t). Jeśli na przykład zapytasz, czy jest większa niż 100, odpowiedź brzmi „tak”. Jeśli zapytasz, co to jest sizeof (size_t) / 2, jak można się domyślić, odpowiedzią jest nadal sizeof (size_t). Jeśli chcesz go wydrukować, możemy zgodzić się na niektóre wyniki. Różnica tych dwóch może być NaN i tak dalej.
Podsumowanie jest takie, że złagodzenie warunku dla size_t ma skończony rozmiar nie zepsuje żadnych programów już istniejących.
PS Przydzielanie pamięci sizeof (size_t) jest nadal możliwe, potrzebujesz tylko policzalnego rozmiaru, więc powiedzmy, że bierzesz wszystkie znaki (lub podobną sztuczkę).
źródło
sizeof
musi zwrócić asize_t
. Musisz więc wybrać określoną wartość.Tak to jest.
1. Cytowana odpowiedź
W reakcji na dużą liczbę głosów negatywnych na moje (i inne) poprawne odpowiedzi - w porównaniu z alarmująco wysoką akceptacją fałszywych odpowiedzi - szukałem mniej teoretycznie głębokiego wyjaśnienia. Znalazłem to . Mam nadzieję, że obejmuje niektóre z typowych błędów, aby pokazać nieco więcej wglądu. Zasadnicza część argumentu:
Krótko mówiąc, ponieważ dla każdej funkcji obliczeniowej istnieje rozwiązanie w języku C (z powodu nieograniczonej liczby górnych granic), każdy problem obliczeniowy ma program w języku C, więc C jest kompletny w Turingu.
2. Moja oryginalna odpowiedź
Powszechne są pomyłki między pojęciami matematycznymi w informatyce teoretycznej (np. Kompletność Turinga) a ich zastosowaniem w świecie rzeczywistym, tj. Technikami w informatyce praktycznej. Kompletność Turinga nie jest własnością fizycznie istniejących maszyn ani żadnego modelu w ograniczonym czasie. To tylko abstrakcyjny obiekt opisujący właściwości teorii matematycznych.
C99 jest kompletny z Turinga bez względu na ograniczenia implementacyjne, tak jak praktycznie każdy inny wspólny język programowania, ponieważ jest w stanie wyrazić funkcjonalnie kompletny zestaw logicznych połączeń i ma w zasadzie dostęp do nieograniczonej ilości pamięci. Ludzie zauważyli, że C wyraźnie ogranicza dostęp do pamięci losowej, ale nie można tego obejść, ponieważ są to dodatkowo określone ograniczenia w standardzie C, podczas gdy kompletność Turinga obejmuje je już bez nich :
Oto bardzo podstawowa rzecz na temat systemów logicznych, która powinna wystarczyć do uzyskania konstruktywnego dowodu. Rozważmy rachunek z pewnymi schematami i regułami aksjomatycznymi, tak że zestaw logicznych konsekwencji wynosi X. Teraz, jeśli dodasz jakieś reguły lub aksjomaty, zestaw logicznych konsekwencji rośnie, tzn. Musi być nadzbiorem X. To na przykład dlatego , logika modalna S4 jest poprawnie zawarta w S5. Podobnie, gdy masz pod-specyfikację, która jest kompletna Turinga, ale dodajesz pewne ograniczenia na górze, nie zapobiegają one żadnej konsekwencjom w X, tj. Musi istnieć sposób na obejście wszystkich ograniczeń. Jeśli chcesz mieć niekompletny język Turinga, rachunek musi być zmniejszony, a nie rozszerzony. Rozszerzenia, które twierdzą, że coś nie byłoby możliwe, ale tak naprawdę tylko dodają niespójności. Te niespójności w standardzie C mogą jednak nie mieć praktycznych konsekwencji, podobnie jak kompletność Turinga nie ma związku z praktycznym zastosowaniem.
Symulowanie dowolnych liczb na podstawie głębokości rekurencji (tj. To ; z możliwością obsługi wielu liczb za pomocą harmonogramu / pseudo-wątków; nie ma teoretycznego ograniczenia głębokości rekurencji w C ), lub użycie pamięci plików do symulacji nieograniczonej pamięci programu ( pomysł ) prawdopodobnie tylko dwie z nieskończonych możliwości konstruktywnego udowodnienia kompletności Turinga C99. Należy pamiętać, że dla obliczalności złożoność czasu i przestrzeni nie ma znaczenia. W szczególności zakładanie ograniczonego środowiska w celu sfałszowania kompletności Turinga jest jedynie okrągłym rozumowaniem, ponieważ ograniczenie to wyklucza wszystkie problemy, które przekraczają założoną złożoność.
( UWAGA : Napisałem tę odpowiedź tylko po to, aby powstrzymać ludzi przed zatrzymaniem się w celu uzyskania intuicji matematycznej z powodu pewnego rodzaju ograniczonego myślenia zorientowanego na aplikację. Szkoda, że większość uczniów przeczyta fałszywie przyjętą odpowiedź z powodu jej głosowania na podstawie fundamentalne wady rozumowania, aby więcej osób rozpowszechniało takie fałszywe przekonania. Jeśli głosujesz za tą odpowiedzią, jesteś tylko częścią problemu).
źródło
CHAR_BITS
.