Là C thực sự Turing-hoàn thành?


40

Tôi đã cố gắng giải thích với ai đó rằng C là Turing-Complete và nhận ra rằng tôi thực sự không biết liệu đó có thực sự là Turing hoàn chỉnh hay không. (C như trong ngữ nghĩa trừu tượng, không phải như trong một triển khai thực tế.)

Câu trả lời "rõ ràng" (đại khái: nó có thể giải quyết một lượng bộ nhớ tùy ý, do đó, nó có thể mô phỏng một máy RAM, do đó, Turing-perfect) thực sự không chính xác, theo như tôi có thể nói, mặc dù tiêu chuẩn C cho phép để size_t lớn tùy ý, nó phải được cố định ở một độ dài nào đó và cho dù nó có cố định ở độ dài nào thì nó vẫn hữu hạn. (Nói cách khác, mặc dù bạn có thể, được cung cấp một máy Turing tạm dừng tùy ý, chọn độ dài size_t để nó chạy "đúng", không có cách nào để chọn độ dài size_t sao cho tất cả các máy Turing tạm dừng sẽ chạy đúng)

Vậy: C99 Turing đã hoàn thành chưa?


3
"Ngữ nghĩa trừu tượng" của C là gì? Chúng được định nghĩa ở bất cứ đâu?
Yuval Filmus

4
@YuvalFilmus - Xem ví dụ ở đây , ví dụ C như được định nghĩa trong tiêu chuẩn trái ngược với ví dụ: "đây là cách gcc làm điều đó".
TLW

1
có "tính kỹ thuật" là các máy tính hiện đại không có bộ nhớ vô hạn như TM nhưng vẫn được coi là "máy tính phổ quát". và lưu ý rằng các ngôn ngữ lập trình trong "ngữ nghĩa" của chúng không thực sự chiếm một bộ nhớ hữu hạn ngoại trừ tất cả các triển khai của chúng tất nhiên bị giới hạn trong bộ nhớ. xem ví dụ: máy tính của chúng tôi hoạt động như một máy Turing . dù sao về cơ bản, tất cả các ngôn ngữ lập trình "chính thống" đều hoàn chỉnh.
vzn

2
C (như máy Turing) không bị giới hạn trong việc sử dụng bộ nhớ máy tính nội bộ cho các tính toán của nó, vì vậy đây thực sự không phải là một đối số hợp lệ chống lại tính hoàn chỉnh của Turing của C.
Revierpost

@reinierpost - Điều đó giống như nói một teletype là thông minh. Điều đó nói rằng "C + tương đương với TM bên ngoài" là Turing-perfect, không phải C là Turing-Complete.
TLW

Câu trả lời:


34

Tôi không chắc nhưng tôi nghĩ câu trả lời là không, vì những lý do khá tinh tế. Tôi đã hỏi về Khoa học máy tính lý thuyết một vài năm trước và không nhận được câu trả lời nào vượt xa những gì tôi sẽ trình bày ở đây.

Trong hầu hết các ngôn ngữ lập trình, bạn có thể mô phỏng máy Turing bằng cách:

  • mô phỏng máy tự động hữu hạn với chương trình sử dụng một lượng bộ nhớ hữu hạn;
  • mô phỏng băng với một cặp danh sách các số nguyên được liên kết, thể hiện nội dung của băng trước và sau vị trí hiện tại. Di chuyển con trỏ có nghĩa là chuyển đầu của một trong các danh sách sang danh sách khác.

Một triển khai cụ thể chạy trên máy tính sẽ hết bộ nhớ nếu băng quá dài, nhưng một triển khai lý tưởng có thể thực hiện chương trình máy Turing một cách trung thực. Điều này có thể được thực hiện bằng bút và giấy hoặc bằng cách mua một máy tính có nhiều bộ nhớ hơn và trình biên dịch nhắm mục tiêu vào một kiến ​​trúc có nhiều bit hơn trên mỗi từ và cứ thế nếu chương trình hết bộ nhớ.

Điều này không hoạt động trong C vì không thể có một danh sách liên kết có thể phát triển mãi mãi: luôn có một số giới hạn về số lượng nút.

Để giải thích tại sao, trước tiên tôi cần giải thích việc triển khai C là gì. C thực sự là một gia đình của ngôn ngữ lập trình. Các tiêu chuẩn ISO C (chính xác hơn, một phiên bản cụ thể của tiêu chuẩn này) định nghĩa (với mức độ trịnh trọng rằng tiếng Anh cho phép) cú pháp và ngữ nghĩa một gia đình của các ngôn ngữ lập trình. C có rất nhiều hành vi không xác địnhhành vi được xác định thực hiện. Một triển khai của CĐT của C của C mã hóa tất cả các hành vi được xác định thực hiện (danh sách những điều cần mã hóa nằm trong phụ lục J cho C99). Mỗi lần thực hiện C là một ngôn ngữ lập trình riêng. Lưu ý rằng ý nghĩa của từ triển khai trên ứng dụng, có một chút đặc biệt: ý nghĩa thực sự của nó là một biến thể ngôn ngữ, có thể có nhiều chương trình biên dịch khác nhau thực hiện cùng một biến thể ngôn ngữ.

2CHAR_BITt2CHAR_BIT×sizeof(t)

2CHAR_BIT×sizeof(void*)

Các giá trị của CHAR_BITsizeof(void*)có thể quan sát được, vì vậy nếu bạn hết bộ nhớ, bạn không thể tiếp tục chạy chương trình của mình với các giá trị lớn hơn cho các tham số đó. Bạn sẽ chạy chương trình theo một ngôn ngữ lập trình khác - triển khai C khác.

n×2CHAR_BIT×sizeof(void*)n

C không trực tiếp áp đặt độ sâu đệ quy tối đa. Việc triển khai được phép có tối đa, nhưng cũng được phép không có. Nhưng làm thế nào để chúng ta liên lạc giữa một cuộc gọi chức năng và cha mẹ của nó? Các đối số sẽ không tốt nếu chúng có thể truy cập được, bởi vì điều đó sẽ gián tiếp giới hạn độ sâu của đệ quy: nếu bạn có một hàm int f(int x) { … f(…) …}thì tất cả các lần xuất hiện xtrên các khung hoạt động fđều có địa chỉ riêng và do đó số lượng các cuộc gọi lồng nhau bị giới hạn bởi số địa chỉ có thể cho x.

Chương trình AC có thể sử dụng lưu trữ không có địa chỉ dưới dạng các registerbiến. Việc triển khai Bình thường chỉ có thể có một số lượng nhỏ các biến hữu hạn không có địa chỉ, nhưng về mặt lý thuyết, việc triển khai có thể cho phép một lượng registerlưu trữ không giới hạn . Trong quá trình triển khai như vậy, bạn có thể thực hiện một số lượng các cuộc gọi đệ quy không giới hạn cho một hàm, miễn là đối số của nó là register. Nhưng vì các đối số là register, bạn không thể tạo một con trỏ cho chúng và vì vậy bạn cần sao chép dữ liệu của chúng xung quanh một cách rõ ràng: bạn chỉ có thể truyền xung quanh một lượng dữ liệu hữu hạn, không phải là cấu trúc dữ liệu có kích thước tùy ý được tạo từ con trỏ.

Với độ sâu đệ quy không giới hạn và hạn chế rằng một hàm chỉ có thể lấy dữ liệu từ người gọi trực tiếp ( registerđối số) và trả lại dữ liệu cho người gọi trực tiếp (giá trị trả về của hàm), bạn có được sức mạnh của tự động đẩy xuống xác định .

Tôi không thể tìm cách đi xa hơn.

(Tất nhiên bạn có thể làm cho chương trình lưu trữ nội dung băng từ bên ngoài, thông qua các chức năng nhập / xuất tệp. Nhưng sau đó bạn sẽ không hỏi liệu C đã hoàn thành Turing chưa, nhưng liệu C cộng với một hệ thống lưu trữ vô hạn có hoàn chỉnh không, Turing mà câu trả lời là nhàm chán, đúng vậy. Bạn cũng có thể định nghĩa bộ lưu trữ là một lời tiên tri Turing - gọi  fopen("oracle", "r+"), fwritenội dung băng ban đầu cho nó và freadquay lại nội dung băng cuối cùng.)


Không rõ ngay tại sao bộ địa chỉ nên hữu hạn. Tôi đã viết một số suy nghĩ trong câu trả lời cho câu hỏi bạn liên kết: cstheory.stackexchange.com/a/37036/43393
Alexey B.

4
Xin lỗi, nhưng theo cùng một logic, không có ngôn ngữ lập trình Turing-Complete nào cả. Mỗi ngôn ngữ có một giới hạn rõ ràng hoặc ẩn trên không gian địa chỉ. Nếu bạn tạo một máy có bộ nhớ vô hạn, thì các con trỏ truy cập ngẫu nhiên rõ ràng cũng sẽ có chiều dài vô hạn. Do đó, nếu máy như vậy xuất hiện, nó sẽ phải cung cấp một bộ hướng dẫn để truy cập bộ nhớ tuần tự, cùng với API cho các ngôn ngữ cấp cao.
IMil

14
@IMil Điều này không đúng. Một số ngôn ngữ lập trình không có giới hạn về không gian địa chỉ, thậm chí không hoàn toàn. Lấy một ví dụ rõ ràng, một máy Turing phổ dụng trong đó trạng thái ban đầu của băng tạo thành chương trình là ngôn ngữ lập trình hoàn chỉnh Turing. Nhiều ngôn ngữ lập trình thực sự được sử dụng trong thực tế có cùng thuộc tính, ví dụ Lisp và SML. Một ngôn ngữ không nhất thiết phải có khái niệm về con trỏ truy cập ngẫu nhiên. (tt)
Gilles 'SO- ngừng trở nên xấu xa'

11
@IMil (tt) Việc triển khai thường làm cho hiệu năng, nhưng chúng tôi biết rằng việc triển khai chạy trên một máy tính cụ thể không phải là Turing-perfect vì nó bị giới hạn bởi kích thước của bộ nhớ máy tính. Nhưng điều đó có nghĩa là việc triển khai không thực hiện toàn bộ ngôn ngữ, chỉ là một tập hợp con (của các chương trình chạy trong N byte bộ nhớ). Bạn có thể chạy chương trình trên máy tính, và nếu nó hết bộ nhớ, hãy chuyển nó sang một máy tính lớn hơn, và cứ như vậy mãi mãi hoặc cho đến khi nó dừng lại. Đó sẽ là một cách hợp lệ để thực hiện toàn bộ ngôn ngữ.
Gilles 'SO- ngừng trở nên xấu xa'

6

Việc bổ sung va_copyAPI của C99 vào API đối số biến đổi có thể cho chúng ta một cánh cửa trở lại cho tính đầy đủ của Turing. Vì nó có thể lặp lại thông qua một danh sách các đối số biến đổi nhiều lần trong một hàm khác với hàm ban đầu nhận được các đối số, va_argscó thể được sử dụng để thực hiện một con trỏ không có con trỏ.

Tất nhiên, một triển khai thực sự của API đối số có thể sẽ có một con trỏ ở đâu đó, nhưng trong cỗ máy trừu tượng của chúng ta, nó có thể được thực hiện bằng phép thuật thay thế.

Đây là bản demo thực hiện tự động đẩy xuống 2 ngăn với các quy tắc chuyển đổi tùy ý:

#include <stdarg.h>
typedef struct { va_list va; } wrapped_stack; // Struct wrapper needed if va_list is an array type.
#define NUM_SYMBOLS /* ... */
#define NUM_STATES /* ... */
typedef enum { NOP, POP1, POP2, PUSH1, PUSH2 } operation_type;
typedef struct { int next_state; operation_type optype; int opsymbol; } transition;
transition transition_table[NUM_STATES][NUM_SYMBOLS][NUM_SYMBOLS] = { /* ... */ };

void step(int state, va_list stack1, va_list stack2);
void push1(va_list stack2, int next_state, ...) {
    va_list stack1;
    va_start(stack1, next_state);
    step(next_state, stack1, stack2);
}
void push2(va_list stack1, int next_state, ...) {
    va_list stack2;
    va_start(stack2, next_state);
    step(next_state, stack1, stack2);
}
void step(int state, va_list stack1, va_list stack2) {
    va_list stack1_copy, stack2_copy;
    va_copy(stack1_copy, stack1); va_copy(stack2_copy, stack2);
    int symbol1 = va_arg(stack1_copy, int), symbol2 = va_arg(stack2_copy, int);
    transition tr = transition_table[state][symbol1][symbol2];
    wrapped_stack ws;
    switch(tr.optype) {
        case NOP: step(tr.next_state, stack1, stack2);
        // Note: attempting to pop the stack's bottom value results in undefined behavior.
        case POP1: ws = va_arg(stack1_copy, wrapped_stack); step(tr.next_state, ws.va, stack2);
        case POP2: ws = va_arg(stack2_copy, wrapped_stack); step(tr.next_state, stack1, ws.va);
        case PUSH1: va_copy(ws.va, stack1); push1(stack2, tr.next_state, tr.opsymbol, ws);
        case PUSH2: va_copy(ws.va, stack2); push2(stack1, tr.next_state, tr.opsymbol, ws);
    }
}
void start_helper1(va_list stack1, int dummy, ...) {
    va_list stack2;
    va_start(stack2, dummy);
    step(0, stack1, stack2);
}
void start_helper0(int dummy, ...) {
    va_list stack1;
    va_start(stack1, dummy);
    start_helper1(stack1, 0, 0);
}
// Begin execution in state 0 with each stack initialized to {0}
void start() {
    start_helper0(0, 0);
}

Lưu ý: Nếu va_listlà một kiểu mảng, thì thực sự có các tham số con trỏ ẩn cho các hàm. Vì vậy, có lẽ sẽ tốt hơn nếu thay đổi loại của tất cả các va_listđối số thành wrapped_stack.


Điều này có thể làm việc. Một mối quan tâm có thể là nó phụ thuộc vào việc phân bổ số lượng va_listbiến tự động không giới hạn stack. Các biến này phải có một địa chỉ &stackvà chúng ta chỉ có thể có một số giới hạn trong số này. Yêu cầu này có thể được phá vỡ bằng cách khai báo mọi biến cục bộ register, có thể?
chi

@chi AIUI một biến không bắt buộc phải có địa chỉ trừ khi có người cố lấy địa chỉ. Ngoài ra, việc tuyên bố lập luận ngay trước dấu chấm lửng là bất hợp pháp register.
frageum

Theo cùng một logic, không nên intbắt buộc phải có một ràng buộc trừ khi ai đó sử dụng ràng buộc hay sizeof(int)?
chi

@chi Không hề. Tiêu chuẩn định nghĩa như là một phần của ngữ nghĩa trừu tượng mà an intcó giá trị giữa một số giới hạn hữu hạn INT_MININT_MAX. Và nếu giá trị của một inttràn các ràng buộc đó, hành vi không xác định xảy ra. Mặt khác, tiêu chuẩn cố ý không yêu cầu tất cả các đối tượng phải có mặt vật lý trong bộ nhớ tại một địa chỉ cụ thể, vì điều này cho phép tối ưu hóa như lưu trữ đối tượng trong một thanh ghi, chỉ lưu trữ một phần của đối tượng, thể hiện nó khác với tiêu chuẩn bố trí, hoặc bỏ qua nó hoàn toàn nếu không cần thiết.
frageum

4

Số học không chuẩn, có thể?

Vì vậy, có vẻ như vấn đề là kích thước hữu hạn sizeof(t). Tuy nhiên, tôi nghĩ rằng tôi biết một công việc xung quanh.

Theo tôi biết, C không yêu cầu triển khai để sử dụng các số nguyên tiêu chuẩn cho loại số nguyên của nó. Do đó, chúng ta có thể sử dụng một mô hình số học không chuẩn . Sau đó, chúng tôi sẽ đặt sizeof(t)thành một số không chuẩn, và bây giờ chúng tôi sẽ không bao giờ đạt được nó trong một số bước hữu hạn. Do đó, độ dài của băng máy Turing sẽ luôn nhỏ hơn "tối đa", vì mức tối đa theo nghĩa đen là không thể đạt được. sizeof(t)đơn giản không phải là một số theo nghĩa thông thường của từ này.

Tất nhiên đây là một kỹ thuật: định lý của Tennenbaum . Nó nói rằng mô hình duy nhất của số học Peano là mô hình chuẩn, điều này rõ ràng sẽ không làm được. Tuy nhiên, theo như tôi biết, C không yêu cầu triển khai sử dụng các kiểu dữ liệu thỏa mãn các tiên đề Peano và cũng không yêu cầu việc thực hiện phải tính toán được, vì vậy đây không phải là vấn đề.

Điều gì sẽ xảy ra nếu bạn cố gắng xuất một số nguyên không chuẩn? Chà, bạn có thể biểu diễn bất kỳ số nguyên không chuẩn nào bằng chuỗi không chuẩn, vì vậy chỉ cần truyền các chữ số từ phía trước chuỗi đó.


Một triển khai sẽ như thế nào?
Revierpost

@reinierpost Tôi đoán nó sẽ đại diện cho dữ liệu bằng cách sử dụng một số mô hình không chuẩn của PA. Nó sẽ tính toán các phép toán số học bằng cách sử dụng một mức độ PA . Tôi nghĩ rằng bất kỳ mô hình như vậy nên cung cấp một triển khai C hợp lệ.
PyRulez

Xin lỗi, điều này không hoạt động. sizeof(t)bản thân nó là một giá trị của loại size_t, vì vậy nó là một số nguyên tự nhiên trong khoảng từ 0 đến SIZE_MAX.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles SIZE_MAX cũng sẽ là một tự nhiên không đạt tiêu chuẩn.
PyRulez

Đây là một cách tiếp cận thú vị. Lưu ý rằng bạn cũng cần ví dụ intptr_t / uintptr_t / ptrdiff_t / intmax_t / uintmax_t là không chuẩn. Trong C ++, điều này sẽ chạy các đảm bảo tiến bộ về phía trước ... không chắc chắn về C.
TLW

0

IMO, một hạn chế mạnh mẽ là không gian địa chỉ (thông qua kích thước con trỏ) là hữu hạn và điều này là không thể phục hồi.

Người ta có thể biện hộ rằng bộ nhớ có thể được "hoán đổi sang đĩa", nhưng tại một số điểm, thông tin địa chỉ sẽ tự vượt quá kích thước địa chỉ.


Đây không phải là điểm chính của câu trả lời được chấp nhận sao? Tôi không nghĩ rằng nó bổ sung bất cứ điều gì mới cho câu trả lời của câu hỏi năm 2016 này.
chi

@chi: không, câu trả lời được chấp nhận không đề cập đến việc hoán đổi bộ nhớ ngoài, có thể được coi là một cách giải quyết.
Yves Daoust

-1

Trong thực tế, những hạn chế này không liên quan đến tính đầy đủ của Turing. Yêu cầu thực sự là cho phép băng dài tùy ý, không vô hạn. Điều đó sẽ tạo ra một vấn đề tạm dừng của một loại khác (vũ trụ "tính toán" băng như thế nào?)

Điều đó là không có thật khi nói rằng "Python không hoàn thành Turing vì bạn không thể tạo một danh sách lớn vô cùng".

[Chỉnh sửa: cảm ơn ông Whitledge đã làm rõ cách chỉnh sửa.]


7
Tôi không nghĩ rằng điều này trả lời quesiton. Câu hỏi đã lường trước câu trả lời này và giải thích lý do tại sao nó không hợp lệ: "mặc dù tiêu chuẩn C cho phép size_t lớn tùy ý, nhưng nó phải được sửa ở một độ dài nào đó và cho dù nó có cố định ở độ dài nào thì nó vẫn hữu hạn ". Bạn có phản ứng nào với lập luận đó không? Tôi không nghĩ chúng ta có thể tính câu hỏi là đã trả lời trừ khi câu trả lời giải thích tại sao lập luận đó sai (hoặc đúng).
DW

5
Tại bất kỳ thời điểm nào, một giá trị của loại size_tlà hữu hạn. Vấn đề là bạn không thể thiết lập một ràng buộc cho size_ttính hợp lệ trong suốt quá trình tính toán: đối với bất kỳ ràng buộc nào, một chương trình có thể vượt quá nó. Nhưng ngôn ngữ C nói rằng tồn tại một ràng buộc cho size_t: trên một triển khai nhất định, nó chỉ có thể tăng lên đến sizeof(size_t)byte. Ngoài ra, hãy tốt đẹp . Nói rằng những người chỉ trích bạn không thể nghĩ về chính họ là thô lỗ.
Gilles 'SO- ngừng trở nên xấu xa'

1
Đây là câu trả lời chính xác. Máy tiện không yêu cầu băng vô hạn, nó yêu cầu băng "dài tùy ý". Đó là, bạn có thể giả định rằng băng là miễn là nó cần phải hoàn thành tính toán. Bạn cũng có thể cho rằng máy tính của bạn có nhiều bộ nhớ như nó cần. Một băng vô hạn là hoàn toàn không cần thiết, bởi vì bất kỳ tính toán nào dừng lại trong thời gian hữu hạn đều có thể sử dụng một lượng băng vô hạn.
Jeffrey L Whitledge

Điều mà câu trả lời này cho thấy là với mỗi TM bạn có thể viết một triển khai C với độ dài con trỏ đủ để mô phỏng nó. Tuy nhiên, không thể viết một triển khai C có thể mô phỏng bất kỳ TM nào . Vì vậy, đặc tả kỹ thuật cấm rằng bất kỳ triển khai cụ thể nào là T-Complete. Bản thân nó cũng không hoàn thành, vì độ dài con trỏ là cố định.

1
Đây là một câu trả lời đúng khác khó có thể nhìn thấy do sự bất lực của hầu hết các cá nhân trong cộng đồng này. Trong khi đó, câu trả lời được chấp nhận là sai và phần bình luận của nó được bảo vệ bởi người điều hành xóa các bình luận quan trọng. Tạm biệt, cs.stackexchange.
xamid

-1

Phương tiện di động cho phép chúng ta tránh được vấn đề bộ nhớ không giới hạn. Có lẽ mọi người sẽ nghĩ rằng đây là một sự lạm dụng, nhưng tôi nghĩ dù sao cũng không thể tránh khỏi.

Sửa chữa bất kỳ thực hiện của một máy Turing phổ quát. Đối với băng, chúng tôi sử dụng phương tiện di động. Khi đầu chạy hết đầu hoặc đầu đĩa hiện tại, máy sẽ nhắc người dùng chèn đĩa tiếp theo hoặc trước đó. Chúng ta có thể sử dụng một điểm đánh dấu đặc biệt để biểu thị đầu bên trái của băng mô phỏng hoặc có một băng không bị ràng buộc theo cả hai hướng.

Điểm mấu chốt ở đây là mọi thứ mà chương trình C phải làm là hữu hạn. Máy tính chỉ cần đủ bộ nhớ để mô phỏng máy tự động, và size_tchỉ cần đủ lớn để cho phép giải quyết lượng bộ nhớ (thực sự khá nhỏ) và trên các đĩa, có thể có kích thước hữu hạn cố định. Vì người dùng chỉ được nhắc chèn đĩa tiếp theo hoặc trước đó, chúng tôi không cần số nguyên lớn không giới hạn để nói "Vui lòng chèn số đĩa 123456 ..."

Tôi cho rằng sự phản đối chính có khả năng liên quan đến người dùng nhưng điều đó dường như là không thể tránh khỏi trong bất kỳ triển khai nào, bởi vì dường như không có cách nào khác để thực hiện bộ nhớ không bị ràng buộc.


3
Tôi cho rằng, trừ khi định nghĩa C yêu cầu bộ nhớ ngoài không bị ràng buộc như vậy, thì điều này không thể được chấp nhận như một bằng chứng về tính đầy đủ của Turing. (ISO 9899 không yêu cầu điều đó, tất nhiên, được viết cho kỹ thuật trong thế giới thực.) Điều tôi quan tâm là, nếu chúng ta chấp nhận điều này, bởi một lý do tương tự, chúng ta có thể cho rằng DFA hoàn thành Turing, vì chúng có thể được sử dụng để lái một đầu trên một băng (lưu trữ bên ngoài).
chi

@chi Tôi không thấy cách lập luận của DFA. Điểm chung của DFA là nó chỉ có quyền truy cập đọc vào bộ lưu trữ. Nếu bạn cho phép nó "lái đầu băng", đó có chính xác là máy Turing không?
David Richerby

2
Thật vậy, tôi đang nitpicking một chút ở đây. Vấn đề là: tại sao bạn có thể thêm "băng" vào C, hãy để C mô phỏng DFA và sử dụng thực tế này để khẳng định rằng C đã hoàn thành Turing, khi chúng ta không thể làm tương tự với DFA? Nếu C không có cách nào để tự thực hiện bộ nhớ không giới hạn, thì nó không nên được coi là Turing hoàn chỉnh. (Tôi vẫn gọi nó là "về mặt đạo đức" Turing hoàn thành, ít nhất, vì giới hạn quá lớn nên trong thực tế, chúng không quan trọng trong hầu hết các trường hợp) Tôi nghĩ rằng để giải quyết dứt điểm vấn đề, người ta sẽ cần một thông số chính thức nghiêm ngặt về C (tiêu chuẩn ISO không đủ)
chi

1
@chi Không sao vì C bao gồm các thường trình I / O của tệp. DFA không.
David Richerby

1
C không hoàn toàn chỉ định những gì các thói quen này làm - hầu hết các ngữ nghĩa của chúng là việc thực hiện được xác định. Việc triển khai AC không bắt buộc để lưu trữ nội dung tệp, ví dụ, tôi nghĩ rằng nó có thể hoạt động như thể mọi tệp là "/ dev / null", có thể nói như vậy. Nó cũng không bắt buộc phải lưu trữ một lượng dữ liệu không giới hạn. Tôi muốn nói rằng lập luận của bạn là chính xác, khi xem xét phần lớn các triển khai C làm gì và khái quát hành vi đó cho một cỗ máy lý tưởng. Nếu chúng ta hoàn toàn dựa vào định nghĩa C, chỉ là, quên đi việc thực hành, tôi không nghĩ nó có hiệu lực.
chi

-2

Chọn size_tlà vô cùng lớn

Bạn có thể chọn size_tlà vô cùng lớn. Đương nhiên, không thể nhận ra việc thực hiện như vậy. Nhưng điều đó không có gì đáng ngạc nhiên, do tính chất hữu hạn của thế giới chúng ta đang sống.

Ý nghĩa thực tiễn

Nhưng ngay cả khi có thể nhận ra việc thực hiện như vậy, sẽ có những vấn đề thực tế. Hãy xem xét các tuyên bố C sau đây:

printf("%zu\n",SIZE_MAX);

SIZE_MAXSIZE_MAXO(2size_t)size_tSIZE_MAXprintf

May mắn thay, vì mục đích lý thuyết của chúng tôi, tôi không thể tìm thấy bất kỳ yêu cầu nào trong đặc điểm kỹ thuật đảm bảo printfsẽ chấm dứt cho tất cả các đầu vào. Vì vậy, theo như tôi biết, chúng tôi không vi phạm đặc điểm kỹ thuật C ở đây.

Về tính đầy đủ tính toán

Vẫn còn phải chứng minh rằng việc thực hiện lý thuyết của chúng tôi là Turing Complete . Chúng tôi có thể hiển thị điều này bằng cách triển khai "bất kỳ máy Turing nào được gõ một lần".

Hầu hết chúng ta có lẽ đã thực hiện Turing Machine như một dự án trường học. Tôi sẽ không cung cấp chi tiết về cách triển khai cụ thể, nhưng đây là một chiến lược thường được sử dụng:

  • Số lượng trạng thái, số ký hiệu và bảng chuyển trạng thái được cố định cho bất kỳ máy nào. Vì vậy, chúng ta có thể biểu diễn các trạng thái và ký hiệu dưới dạng số và bảng chuyển trạng thái dưới dạng mảng 2 chiều.
  • Các băng có thể được đại diện như một danh sách liên kết. Chúng ta có thể sử dụng một danh sách liên kết đôi hoặc hai danh sách liên kết đơn (một cho mỗi hướng từ vị trí hiện tại).

Bây giờ hãy xem những gì cần thiết để nhận ra một triển khai như vậy:

  • Khả năng đại diện cho một số bộ số cố định, nhưng lớn tùy ý. Để đại diện cho bất kỳ số tùy ý, chúng tôi chọn MAX_INTlà vô hạn. (Ngoài ra, chúng ta có thể sử dụng các đối tượng khác để thể hiện các trạng thái và ký hiệu.)
  • Khả năng xây dựng một danh sách liên kết lớn tùy ý cho băng của chúng tôi. Một lần nữa, không có giới hạn hữu hạn về kích thước. Điều này có nghĩa là chúng tôi không thể xây dựng danh sách này trước, vì chúng tôi sẽ dành mãi mãi chỉ để xây dựng băng của chúng tôi. Nhưng, chúng ta có thể xây dựng danh sách này dần dần nếu chúng ta sử dụng cấp phát bộ nhớ động. Chúng ta có thể sử dụng malloc, nhưng chúng ta phải xem xét thêm một chút nữa:
    • Đặc tả C cho phép mallocthất bại nếu, ví dụ, bộ nhớ khả dụng đã cạn kiệt. Vì vậy, việc thực hiện của chúng tôi chỉ thực sự phổ quát nếu mallockhông bao giờ thất bại.
    • Tuy nhiên, nếu việc triển khai của chúng tôi được chạy trên một máy có bộ nhớ vô hạn, thì không cần phải mallocthất bại. Không vi phạm tiêu chuẩn C, việc thực hiện của chúng tôi sẽ đảm bảo rằng mallocsẽ không bao giờ thất bại.
  • Khả năng xác định con trỏ, tìm kiếm các phần tử mảng và truy cập các thành viên của một nút danh sách được liên kết.

Vì vậy, danh sách trên là những gì cần thiết để triển khai Máy Turing trong triển khai C giả định của chúng tôi. Các tính năng này phải chấm dứt. Tuy nhiên, bất cứ điều gì khác, có thể được phép không chấm dứt (trừ khi tiêu chuẩn yêu cầu). Điều này bao gồm số học, IO, v.v.


6
Điều gì sẽ printf("%zu\n",SIZE_MAX);in trên một thực hiện như vậy?
Ruslan

1
@Ruslan, việc triển khai như vậy là không thể, giống như không thể thực hiện máy Turing. Nhưng nếu việc triển khai như vậy là có thể, tôi tưởng tượng nó sẽ in một số biểu diễn thập phân của một số lượng lớn vô hạn - có lẽ là một dòng chữ số thập phân vô hạn.
Nathan Davis

2
@NathanDavis Có thể triển khai máy Turing. Mẹo nhỏ là bạn không cần phải xây dựng một băng vô hạn - bạn chỉ cần xây dựng phần được sử dụng của băng tăng dần khi cần thiết.
Gilles 'SO- ngừng trở nên xấu xa'

2
@Gilles: Trong vũ trụ hữu hạn mà chúng ta đang sống, không thể thực hiện một máy Turing.
gnasher729

1
@NathanDavis Nhưng nếu bạn làm điều đó, thì bạn đã thay đổi sizeof(size_t)(hoặc CHAR_BITS). Bạn không thể tiếp tục từ trạng thái mới, bạn phải bắt đầu lại, nhưng việc thực hiện chương trình có thể khác bây giờ vì các hằng số đó khác nhau
Gilles 'SO- ngừng trở nên xấu xa'

-2

Đối số chính ở đây là kích thước của size_t là hữu hạn, mặc dù có thể vô cùng lớn.

Có một cách giải quyết cho nó, mặc dù tôi không chắc liệu điều này có trùng với ISO C.

Giả sử bạn có một máy có bộ nhớ vô hạn. Do đó, bạn không bị giới hạn kích thước con trỏ. Bạn vẫn có loại size_t của bạn. Nếu bạn hỏi tôi sizeof (size_t) là gì thì câu trả lời sẽ chỉ là sizeof (size_t). Nếu bạn hỏi nếu nó lớn hơn 100 chẳng hạn thì câu trả lời là có. Nếu bạn hỏi sizeof (size_t) / 2 là gì thì bạn có thể đoán câu trả lời vẫn là sizeof (size_t). Nếu bạn muốn in nó, chúng tôi có thể đồng ý về một số đầu ra. Sự khác biệt của hai có thể là NaN và như vậy.

Tóm tắt là việc nới lỏng điều kiện cho size_t có kích thước hữu hạn sẽ không phá vỡ bất kỳ chương trình nào đã tồn tại.

PS Phân bổ kích thước bộ nhớ (size_t) vẫn có thể, bạn chỉ cần kích thước có thể đếm được, vì vậy giả sử bạn lấy tất cả evens (hoặc thủ thuật tương tự).


1
"Sự khác biệt của hai loại này có thể là NaN". Không, nó không thể được. Không có thứ gọi là NaN loại nguyên trong C.
TLW

Theo tiêu chuẩn, sizeofphải trả lại a size_t. Vì vậy, bạn phải chọn một số giá trị cụ thể.
Draconis

-4

Đúng vậy

1. Trích dẫn câu trả lời

Như một phản ứng về số lượng downvote cao đối với các câu trả lời đúng của tôi (và khác) - so với sự chấp thuận cao đáng báo động của các câu trả lời sai - tôi đã tìm kiếm một lời giải thích thay thế ít sâu sắc về mặt lý thuyết. Tôi tìm thấy này một. Tôi hy vọng nó bao gồm một số sai lầm phổ biến ở đây, để một cái nhìn sâu sắc hơn được hiển thị. Phần thiết yếu của lập luận:

[...] Lập luận của anh ấy như sau: giả sử người ta đã viết một chương trình chấm dứt nhất định có thể yêu cầu, trong quá trình thực thi, lên đến một số lượng lưu trữ tùy ý. Không thay đổi chương trình đó, có thể một posteriori thực hiện một phần cứng máy tính và trình biên dịch C của nó cung cấp đủ dung lượng để chứa tính toán này. Điều này có thể yêu cầu mở rộng độ rộng của char (thông qua CHAR_BITS) và / hoặc con trỏ (thông qua size_t), nhưng chương trình sẽ không cần phải sửa đổi. Bởi vì điều này là có thể, C thực sự là Turing-Complete để chấm dứt các chương trình.

Phần khó khăn của lập luận này là nó chỉ hoạt động khi xem xét chấm dứt các chương trình. Các chương trình kết thúc có thuộc tính tốt này là chúng có giới hạn trên tĩnh đối với yêu cầu lưu trữ của chúng, người ta có thể xác định bằng thực nghiệm bằng cách chạy chương trình qua đầu vào mong muốn với kích thước lưu trữ tăng dần, cho đến khi nó phù hợp.

Lý do tại sao tôi đã đánh lạc hướng trong suy nghĩ của mình rằng tôi đang xem xét lớp rộng hơn của các chương trình không kết thúc có ích của [[]]

Nói tóm lại, vì đối với mọi hàm tính toán đều có một giải pháp trong ngôn ngữ C (do giới hạn trên không giới hạn), mọi vấn đề tính toán đều có chương trình C, do đó C hoàn thành Turing.

2. Câu trả lời ban đầu của tôi

Có sự nhầm lẫn phổ biến giữa các khái niệm toán học trong khoa học máy tính lý thuyết (chẳng hạn như Turing-đầy đủ) và ứng dụng trong thế giới thực của chúng, tức là các kỹ thuật trong khoa học máy tính thực tế. Turing-đầy đủ không phải là một tài sản của các máy hiện có hoặc bất kỳ mô hình giới hạn không thời gian nào. Nó chỉ là một đối tượng trừu tượng mô tả các thuộc tính của các lý thuyết toán học.

C99 hoàn thành Turing bất kể các hạn chế dựa trên triển khai, giống như hầu hết các ngôn ngữ lập trình phổ biến khác, vì nó có thể diễn đạt một tập hợp các kết nối logic đầy đủ chức năng và có quyền truy cập vào bộ nhớ không giới hạn. Mọi người đã chỉ ra rằng C hạn chế truy cập bộ nhớ ngẫu nhiên một cách rõ ràng bị hạn chế, nhưng điều này không có gì là không thể tránh được, vì đây là những hạn chế được nêu thêm trong tiêu chuẩn C, trong khi tính đầy đủ của Turing không có:

Đây là một điều rất cơ bản về các hệ thống logic nên đủ cho một bằng chứng không mang tính xây dựng . Hãy xem xét một phép tính với một số sơ đồ và quy tắc tiên đề, sao cho tập hợp các điều kiện logic là X. Bây giờ nếu bạn thêm một số quy tắc hoặc tiên đề, tập hợp các hậu quả logic sẽ phát triển, ví dụ: phải là siêu bộ của X. , logic phương thức S4 được chứa đúng trong S5. Tương tự, khi bạn có một đặc tả con là Turing-Complete, nhưng bạn thêm một số hạn chế lên trên, những điều này không ngăn chặn bất kỳ hậu quả nào trong X, tức là phải có cách để tránh tất cả các hạn chế. Nếu bạn muốn một ngôn ngữ không hoàn chỉnh, tính toán phải được giảm bớt, không được mở rộng. Các phần mở rộng tuyên bố rằng một cái gì đó sẽ không thể, nhưng thực tế là, chỉ thêm sự không nhất quán. Những sự không nhất quán trong tiêu chuẩn C có thể không mang lại hậu quả thực tế nào, giống như tính hoàn chỉnh của Turing không liên quan đến ứng dụng thực tế.

Mô phỏng các số tùy ý dựa trên độ sâu đệ quy (nghĩa là này , với khả năng hỗ trợ nhiều số thông qua lập lịch / giả mã; không có giới hạn lý thuyết nào cho độ sâu đệ quy trong C ) hoặc sử dụng lưu trữ tệp để mô phỏng bộ nhớ chương trình không giới hạn ( ý tưởng ) có lẽ chỉ có hai trong số các khả năng vô hạn để chứng minh một cách xây dựng tính hoàn chỉnh của C99. Chúng ta nên nhớ rằng đối với khả năng tính toán, thời gian và độ phức tạp không gian là không liên quan. Cụ thể, giả sử một môi trường hạn chế để làm sai lệch tính đầy đủ của Turing chỉ là lý do vòng tròn vì giới hạn đó loại trừ tất cả các vấn đề vượt quá độ phức tạp giả định trước.

( LƯU Ý : Tôi chỉ viết câu trả lời này để ngăn mọi người dừng lại để có được trực giác toán học do một số loại suy nghĩ hạn chế theo định hướng ứng dụng. Thật đáng tiếc khi hầu hết người học sẽ đọc câu trả lời sai được chấp nhận do nó được đưa lên dựa trên sai sót cơ bản của lý luận, để nhiều người sẽ truyền bá niềm tin sai lầm như vậy. Nếu bạn hạ thấp câu trả lời này, bạn chỉ là một phần của vấn đề.)


4
Tôi không theo dõi đoạn cuối của bạn. Bạn cho rằng việc thêm các hạn chế làm tăng sức mạnh biểu cảm, nhưng điều đó rõ ràng không đúng. Hạn chế chỉ có thể làm giảm sức mạnh biểu cảm. Ví dụ: nếu bạn lấy C và thêm hạn chế rằng không có chương trình nào có thể truy cập hơn 640kb dung lượng lưu trữ (dưới bất kỳ hình thức nào), thì bạn đã biến nó thành một máy tự động hữu hạn ưa thích rõ ràng không hoàn thành Turing.
David Richerby

3
Nếu bạn có dung lượng lưu trữ cố định, bạn không thể mô phỏng bất cứ thứ gì cần nhiều tài nguyên hơn bạn có. Chỉ có rất nhiều cấu hình mà bộ nhớ của bạn có thể có, có nghĩa là chỉ có rất nhiều thứ bạn có thể làm.
David Richerby

2
Tôi không hiểu lý do tại sao bạn đề cập đến "máy móc hiện có". Lưu ý rằng Turing-đầy đủ là một thuộc tính của mô hình tính toán toán học, không phải của các hệ thống vật lý. Tôi đồng ý rằng không có hệ thống vật lý nào, là một vật thể hữu hạn, có thể tiến gần đến sức mạnh của máy Turing, nhưng điều đó không liên quan. Chúng ta vẫn có thể sử dụng bất kỳ ngôn ngữ lập trình nào, xem xét định nghĩa toán học về ngữ nghĩa của nó và kiểm tra xem đối tượng toán học đó đã hoàn thành chưa. Trò chơi cuộc sống của Conway là Turing mạnh mẽ ngay cả khi không thể thực hiện được.
chi

2
@xamid Nếu bạn lo ngại về các chính sách kiểm duyệt của trang web này, hãy đưa nó đến Máy tính Khoa học Meta . Cho đến lúc đó, xin vui lòng được tốt đẹp . Lạm dụng bằng lời nói của người khác sẽ không được dung thứ. (Tôi đã xóa tất cả các nhận xét không liên quan đến vấn đề trong tay.)
Raphael

2
Bạn nói rằng sửa đổi độ rộng của một con trỏ sẽ không thay đổi chương trình, nhưng các chương trình có thể đọc chiều rộng của một con trỏ và làm bất cứ điều gì chúng muốn với giá trị đó. Tương tự cho CHAR_BITS.
Draconis
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.