Như được đề xuất trong câu trả lời này , đó là vấn đề hỗ trợ phần cứng, mặc dù truyền thống trong thiết kế ngôn ngữ cũng đóng một vai trò.
Khi một hàm trả về, nó để lại một con trỏ tới đối tượng trả về trong một thanh ghi cụ thể
Trong ba ngôn ngữ đầu tiên, Fortran, Lisp và COBOL, ngôn ngữ đầu tiên sử dụng một giá trị trả về duy nhất vì nó được mô hình hóa trên toán học. Cái thứ hai trả về một số lượng tham số tùy ý giống như cách nó nhận được: dưới dạng một danh sách (cũng có thể lập luận rằng nó chỉ được truyền và trả về một tham số duy nhất: địa chỉ của danh sách). Giá trị thứ ba trả về 0 hoặc 1 giá trị.
Những ngôn ngữ đầu tiên này ảnh hưởng rất nhiều đến thiết kế của các ngôn ngữ đi theo chúng, mặc dù ngôn ngữ duy nhất trả về nhiều giá trị, Lisp, không bao giờ thu hút được nhiều sự phổ biến.
Khi C đến, trong khi bị ảnh hưởng bởi các ngôn ngữ trước nó, nó đã tập trung rất nhiều vào việc sử dụng hiệu quả tài nguyên phần cứng, giữ mối liên hệ chặt chẽ giữa những gì ngôn ngữ C đã làm và mã máy thực hiện nó. Một số tính năng lâu đời nhất của nó, chẳng hạn như các biến "tự động" và "đăng ký", là kết quả của triết lý thiết kế đó.
Cũng phải chỉ ra rằng ngôn ngữ lắp ráp đã phổ biến rộng rãi cho đến những năm 80, khi cuối cùng nó bắt đầu bị loại bỏ khỏi sự phát triển chính thống. Những người viết trình biên dịch và tạo ngôn ngữ đã quen thuộc với lắp ráp, và, phần lớn, giữ cho những gì hoạt động tốt nhất ở đó.
Hầu hết các ngôn ngữ tách khỏi quy tắc này không bao giờ phổ biến, và do đó, không bao giờ đóng vai trò mạnh mẽ ảnh hưởng đến quyết định của các nhà thiết kế ngôn ngữ (tất nhiên, những người được truyền cảm hứng từ những gì họ biết).
Vì vậy, hãy đi kiểm tra ngôn ngữ lắp ráp. Trước tiên, hãy nhìn vào 6502 , một bộ vi xử lý năm 1975 được sử dụng nổi tiếng bởi các máy vi tính Apple II và VIC-20. Nó rất yếu so với những gì được sử dụng trong máy tính lớn và máy tính mini thời bấy giờ, mặc dù mạnh mẽ so với các máy tính đầu tiên của 20, 30 năm trước, vào buổi bình minh của ngôn ngữ lập trình.
Nếu bạn nhìn vào mô tả kỹ thuật, nó có 5 thanh ghi cộng với một vài cờ một bit. Thanh ghi "đầy đủ" duy nhất là Bộ đếm chương trình (PC) - thanh ghi đó trỏ đến lệnh tiếp theo sẽ được thực thi. Các thanh ghi khác trong đó bộ tích lũy (A), hai thanh ghi "chỉ mục" (X và Y) và con trỏ ngăn xếp (SP).
Gọi một chương trình con sẽ đặt PC vào bộ nhớ được chỉ ra bởi SP, và sau đó làm giảm SP. Trở về từ một chương trình con làm việc ngược lại. Người ta có thể đẩy và kéo các giá trị khác trên ngăn xếp, nhưng rất khó để tham chiếu bộ nhớ liên quan đến SP, vì vậy việc viết các chương trình con được đăng ký lại rất khó khăn. Điều này chúng ta coi là đương nhiên, gọi một chương trình con bất cứ lúc nào chúng ta cảm thấy, không quá phổ biến trên kiến trúc này. Thông thường, một "ngăn xếp" riêng biệt sẽ được tạo để các tham số và địa chỉ trả về chương trình con sẽ được giữ riêng.
Nếu bạn nhìn vào bộ xử lý đã truyền cảm hứng cho 6502, 6800 , nó có một thanh ghi bổ sung, Thanh ghi chỉ mục (IX), rộng như SP, có thể nhận giá trị từ SP.
Trên máy, việc gọi một chương trình con được đăng ký lại bao gồm đẩy các tham số trên ngăn xếp, đẩy PC, thay đổi PC sang địa chỉ mới và sau đó chương trình con sẽ đẩy các biến cục bộ của nó lên ngăn xếp . Vì số lượng biến và tham số cục bộ đã biết, việc giải quyết chúng có thể được thực hiện liên quan đến ngăn xếp. Ví dụ, một hàm nhận hai tham số và có hai biến cục bộ sẽ trông như thế này:
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
Nó có thể được gọi bất kỳ số lần vì tất cả không gian tạm thời nằm trên ngăn xếp.
Các 8080 , được sử dụng trên TRS-80 và một loạt các CP / M vi tính dựa trên có thể làm điều gì đó tương tự như 6800, bằng cách đẩy SP trên stack và sau đó popping nó trên đăng ký gián tiếp của nó, HL.
Đây là một cách rất phổ biến để thực hiện mọi thứ và nó thậm chí còn hỗ trợ nhiều hơn cho các bộ xử lý hiện đại hơn, với Con trỏ cơ sở giúp loại bỏ tất cả các biến cục bộ trước khi trở lại dễ dàng.
Vấn đề là, làm thế nào để bạn trả lại bất cứ điều gì ? Các bộ đăng ký bộ xử lý không có nhiều từ rất sớm và người ta thường cần sử dụng một số trong số chúng thậm chí để tìm ra bộ nhớ nào cần xử lý. Việc trả lại mọi thứ trên ngăn xếp sẽ rất phức tạp: bạn phải bật mọi thứ, lưu PC, đẩy các tham số trả về (sẽ được lưu trữ trong khi đó?), Sau đó đẩy PC lại và quay lại.
Vì vậy, những gì thường được thực hiện là đặt trước một đăng ký cho giá trị trả lại. Mã gọi biết giá trị trả về sẽ nằm trong một thanh ghi cụ thể, sẽ phải được giữ lại cho đến khi có thể được lưu hoặc sử dụng.
Hãy xem xét một ngôn ngữ cho phép nhiều giá trị trả về: Forth. Những gì Forth làm là giữ một ngăn xếp trả lại (RP) và ngăn xếp dữ liệu (SP) riêng biệt, sao cho tất cả một chức năng phải làm là bật tất cả các tham số của nó và để lại các giá trị trả về trên ngăn xếp. Vì ngăn xếp trả lại là riêng biệt, nó không cản trở.
Là một người đã học ngôn ngữ lắp ráp và Forth trong sáu tháng đầu tiên trải nghiệm với máy tính, nhiều giá trị trả về trông hoàn toàn bình thường đối với tôi. Các toán tử như Forth's /mod
, trả về phép chia số nguyên và phần còn lại, có vẻ hiển nhiên. Mặt khác, tôi có thể dễ dàng thấy một người có kinh nghiệm ban đầu là tâm trí C thấy khái niệm đó kỳ lạ như thế nào: nó đi ngược lại những kỳ vọng đã ăn sâu của họ về "chức năng" là gì.
Đối với môn toán ... tốt, tôi đã lập trình máy tính theo cách trước khi tôi có được các chức năng trong các lớp toán. Có là một phần của toàn bộ CS và các ngôn ngữ lập trình mà bị ảnh hưởng bởi toán học, nhưng, sau đó một lần nữa, có cả một bộ phận mà không phải là.
Vì vậy, chúng ta có sự hợp nhất của các yếu tố trong đó toán học ảnh hưởng đến thiết kế ngôn ngữ ban đầu, trong đó các ràng buộc về phần cứng chỉ ra những gì dễ dàng thực hiện và các ngôn ngữ phổ biến ảnh hưởng đến cách thức phát triển phần cứng (máy Lisp và bộ xử lý máy Forth trong quá trình này).