Tại sao các hàm trong Ocaml / F # không được đệ quy theo mặc định?


104

Tại sao các hàm trong F # và Ocaml (và có thể cả các ngôn ngữ khác) không phải là hàm đệ quy mặc định?

Nói cách khác, tại sao các nhà thiết kế ngôn ngữ lại quyết định bắt bạn nhập recmột cách rõ ràng vào một khai báo như:

let rec foo ... = ...

và không cung cấp cho hàm khả năng đệ quy theo mặc định? Tại sao lại cần một reccấu trúc rõ ràng ?


Câu trả lời:


87

Các hậu duệ của Pháp và Anh của ML nguyên bản đã có những lựa chọn khác nhau và những lựa chọn của họ đã được kế thừa qua nhiều thập kỷ cho các biến thể hiện đại. Vì vậy, đây chỉ là di sản nhưng nó ảnh hưởng đến các thành ngữ trong các ngôn ngữ này.

Các hàm không phải là đệ quy theo mặc định trong họ ngôn ngữ CAML của Pháp (bao gồm cả OCaml). Lựa chọn này giúp bạn dễ dàng thực hiện các định nghĩa hàm (và biến) bằng cách sử dụng letcác ngôn ngữ đó bởi vì bạn có thể tham khảo định nghĩa trước đó bên trong phần nội dung của định nghĩa mới. F # kế thừa cú pháp này từ OCaml.

Ví dụ: siêu cấp hàm pkhi tính toán entropy Shannon của một chuỗi trong OCaml:

let shannon fold p =
  let p x = p x *. log(p x) /. log 2.0 in
  let p t x = t +. p x in
  -. fold p 0.0

Lưu ý cách đối số pcho hàm bậc cao hơn shannonđược bổ sung bởi một đối số khác ptrong dòng đầu tiên của phần nội dung và sau đó là đối số khácp ở dòng thứ hai của phần nội dung.

Ngược lại, nhánh SML ở Anh của họ ngôn ngữ ML có lựa chọn khác và các funhàm -bound của SML là đệ quy theo mặc định. Khi hầu hết các định nghĩa hàm không cần quyền truy cập vào các ràng buộc trước đó của tên hàm của chúng, điều này dẫn đến mã đơn giản hơn. Tuy nhiên, các hàm siêu cưỡng bức được tạo ra để sử dụng các tên khác nhau ( f1, f2v.v.) gây ô nhiễm phạm vi và có thể vô tình gọi sai "phiên bản" của một hàm. Và bây giờ có sự khác biệt giữa các hàm liên kết đệ quy ngầm định và các funhàm liên kết không đệ quy val.

Haskell làm cho nó có thể suy ra sự phụ thuộc giữa các định nghĩa bằng cách hạn chế chúng là thuần túy. Điều này làm cho các mẫu đồ chơi trông đơn giản hơn nhưng lại có giá rất đắt ở những nơi khác.

Lưu ý rằng các câu trả lời do Ganesh và Eddie đưa ra là những sợi dây màu đỏ. Họ giải thích tại sao các nhóm hàm không thể được đặt bên trong một khối khổng lồ let rec ... and ...vì nó ảnh hưởng đến thời điểm các biến kiểu được tổng quát hóa. Điều này không liên quan gì đến việc recđược mặc định trong SML nhưng không liên quan đến OCaml.


3
Tôi không nghĩ rằng chúng là những con cá đỏ: nếu không phải vì những hạn chế về suy luận, có khả năng toàn bộ chương trình hoặc mô-đun sẽ tự động được coi là đệ quy lẫn nhau như hầu hết các ngôn ngữ khác. Điều đó sẽ đưa ra quyết định thiết kế cụ thể về việc có nên yêu cầu "rec" hay không.
GS - Xin lỗi Monica

"... tự động được coi là đệ quy lẫn nhau như hầu hết các ngôn ngữ khác". BASIC, C, C ++, Clojure, Erlang, F #, Factor, Forth, Fortran, Groovy, OCaml, Pascal, Smalltalk và Standard ML thì không.
JD

3
C / C ++ chỉ yêu cầu các nguyên mẫu cho các định nghĩa chuyển tiếp, điều này không thực sự về việc đánh dấu đệ quy một cách rõ ràng. Java, C # và Perl chắc chắn có đệ quy ngầm định. Chúng ta có thể tranh cãi bất tận về ý nghĩa của "hầu hết" và tầm quan trọng của mỗi ngôn ngữ, vì vậy hãy giải quyết cho "rất nhiều" các ngôn ngữ khác.
GS - Xin lỗi Monica

3
"C / C ++ chỉ yêu cầu các nguyên mẫu cho các định nghĩa chuyển tiếp, điều này không thực sự về việc đánh dấu đệ quy một cách rõ ràng". Chỉ trong trường hợp đặc biệt của tự đệ quy. Trong trường hợp chung của đệ quy lẫn nhau, khai báo chuyển tiếp là bắt buộc trong cả C và C ++.
JD

2
Trên thực tế, khai báo chuyển tiếp không bắt buộc trong C ++ trong phạm vi lớp, tức là các phương thức tĩnh có thể gọi nhau mà không cần khai báo.
polkovnikov.ph

52

Một lý do quan trọng cho việc sử dụng rõ ràng rec là liên quan đến suy luận kiểu Hindley-Milner, làm cơ sở cho tất cả các ngôn ngữ lập trình hàm được định kiểu tĩnh (mặc dù đã thay đổi và mở rộng theo nhiều cách khác nhau).

Nếu bạn có một định nghĩa let f x = x, bạn sẽ mong đợi nó có kiểu 'a -> 'avà có thể áp dụng cho các 'akiểu khác nhau ở các điểm khác nhau. Nhưng tương tự, nếu bạn viết let g x = (x + 1) + ..., bạn sẽ xđược coi là một intphần còn lại của phần còn lại củag .

Cách suy luận của Hindley-Milner giải quyết sự khác biệt này là thông qua một bước tổng quát hóa rõ ràng . Tại một số điểm nhất định khi xử lý chương trình của bạn, hệ thống kiểu dừng lại và nói "được rồi, các loại định nghĩa này sẽ được tổng quát hóa vào thời điểm này, để khi ai đó sử dụng chúng, mọi biến kiểu tự do trong kiểu của chúng sẽ được làm mới khởi tạo và do đó sẽ không ảnh hưởng đến bất kỳ cách sử dụng nào khác của định nghĩa này. "

Nó chỉ ra rằng nơi hợp lý để thực hiện tổng quát hóa này là sau khi kiểm tra một bộ hàm đệ quy lẫn nhau. Bất kỳ điều gì sớm hơn, và bạn sẽ khái quát hóa quá nhiều, dẫn đến tình huống mà các loại thực sự có thể va chạm. Bất cứ lúc nào sau đó, và bạn sẽ khái quát quá ít, tạo ra các định nghĩa không thể được sử dụng với nhiều kiểu thuyết minh.

Vì vậy, với điều kiện trình kiểm tra kiểu cần biết về các tập hợp định nghĩa nào là đệ quy lẫn nhau, nó có thể làm gì? Một khả năng là chỉ cần thực hiện phân tích sự phụ thuộc vào tất cả các định nghĩa trong một phạm vi và sắp xếp lại chúng thành các nhóm nhỏ nhất có thể. Haskell thực sự làm được điều này, nhưng trong các ngôn ngữ như F # (và OCaml và SML) có tác dụng phụ không hạn chế, đây là một ý tưởng tồi vì nó cũng có thể sắp xếp lại các tác dụng phụ. Vì vậy, thay vào đó, nó yêu cầu người dùng đánh dấu rõ ràng các định nghĩa nào là đệ quy lẫn nhau và do đó bằng cách mở rộng nơi xảy ra tổng quát hóa.


3
Ồ, không. Đoạn đầu tiên của bạn sai (bạn đang nói về việc sử dụng rõ ràng "và" chứ không phải "rec") và do đó, phần còn lại không liên quan.
JD

5
Tôi không bao giờ hài lòng với yêu cầu này. Cảm ơn vì lời giải thích. Một lý do khác khiến Haskell vượt trội về thiết kế.
Bent Rasmussen

9
KHÔNG!!!! LÀM THẾ NÀO ĐIỀU NÀY CÓ THỂ XẢY RA?! Câu trả lời này rõ ràng là sai! Vui lòng đọc câu trả lời của Harrop bên dưới hoặc xem Định nghĩa của Standard ML (Milner, Tofte, Harper, MacQueen - 1997) [p.24]
lambdapower

9
Như tôi đã nói trong câu trả lời của mình, vấn đề suy luận kiểu là một trong những lý do cho sự cần thiết của rec, thay vì là lý do duy nhất. Câu trả lời của Jon cũng là một câu trả lời rất hợp lệ (ngoài những lời bình thường về Haskell); Tôi không nghĩ hai người đối lập nhau.
GS - Xin lỗi Monica

16
"vấn đề suy luận kiểu là một trong những lý do cho sự cần thiết của rec". Thực tế là OCaml yêu cầu recnhưng SML không yêu cầu là một ví dụ phản đối rõ ràng. Nếu suy luận kiểu là vấn đề vì những lý do bạn mô tả, OCaml và SML không thể chọn các giải pháp khác nhau như họ đã làm. Tất nhiên, lý do là bạn đang nói andđến để làm cho Haskell có liên quan.
JD

10

Có hai lý do chính khiến đây là một ý kiến ​​hay:

Đầu tiên, nếu bạn bật định nghĩa đệ quy thì bạn không thể tham chiếu đến một liên kết trước đó của một giá trị có cùng tên. Đây thường là một thành ngữ hữu ích khi bạn đang làm điều gì đó như mở rộng một mô-đun hiện có.

Thứ hai, các giá trị đệ quy, và đặc biệt là các tập giá trị đệ quy lẫn nhau, khó lập luận hơn nhiều khi đó là các định nghĩa tiến hành theo thứ tự, mỗi định nghĩa mới được xây dựng dựa trên những gì đã được xác định. Thật tuyệt khi đọc mã như vậy để đảm bảo rằng, ngoại trừ các định nghĩa được đánh dấu rõ ràng là đệ quy, các định nghĩa mới chỉ có thể tham chiếu đến các định nghĩa trước đó.


4

Một số phỏng đoán:

  • letkhông chỉ được sử dụng để ràng buộc các hàm mà còn cả các giá trị thông thường khác. Hầu hết các dạng giá trị không được phép đệ quy. Cho phép một số dạng giá trị đệ quy nhất định (ví dụ: hàm, biểu thức lười biếng, v.v.), vì vậy nó cần một cú pháp rõ ràng để chỉ ra điều này.
  • Có thể dễ dàng hơn để tối ưu hóa các hàm không đệ quy
  • Bao đóng được tạo khi bạn tạo một hàm đệ quy cần phải bao gồm một mục trỏ đến chính hàm đó (vì vậy hàm có thể gọi một cách đệ quy chính nó), điều này làm cho các bao đóng đệ quy phức tạp hơn các bao đóng không đệ quy. Vì vậy, sẽ rất tuyệt nếu có thể tạo các bao đóng không đệ quy đơn giản hơn khi bạn không cần đệ quy
  • Nó cho phép bạn xác định một hàm theo giá trị hoặc hàm đã được xác định trước đó của cùng tên; mặc dù tôi nghĩ đây là một thực hành xấu
  • Thêm an toàn? Đảm bảo rằng bạn đang làm những gì bạn dự định. Ví dụ: Nếu bạn không có ý định nó là đệ quy nhưng bạn đã vô tình sử dụng một tên bên trong hàm trùng với tên của chính hàm, nó rất có thể sẽ khiếu nại (trừ khi tên đã được xác định trước đó)
  • Cấu lettrúc tương tự như letcấu trúc trong Lisp và Scheme; không đệ quy. Có một letreccấu trúc riêng biệt trong Scheme cho đệ quy, hãy

"Hầu hết các dạng giá trị không được phép là đệ quy. Một số dạng của giá trị đệ quy được phép (ví dụ: hàm, biểu thức lười biếng, v.v.), vì vậy nó cần một cú pháp rõ ràng để chỉ ra điều này". Điều đó đúng với F # nhưng tôi không chắc nó đúng với OCaml nơi bạn có thể làm như thế nào let rec xs = 0::ys and ys = 1::xs.
JD

4

Đưa ra điều này:

let f x = ... and g y = ...;;

Đối chiếu:

let f a = f (g a)

Với cái này:

let rec f a = f (g a)

Định nghĩa lại trước đây fđể áp dụngf vào kết quả của việc áp dụng gcho a. Cái sau định nghĩa lại fvòng lặp mãi mãi áp dụng gchoa , mà thường là không phải những gì bạn muốn trong ML biến thể.

Điều đó nói rằng, đó là một phong cách thiết kế ngôn ngữ. Chỉ cần đi với nó.


1

Một phần lớn của nó là nó cho phép lập trình viên kiểm soát nhiều hơn độ phức tạp của phạm vi cục bộ của họ. Quang phổ của let, let*let recđề nghị một mức độ ngày càng tăng của cả hai quyền lực và chi phí. let*let recvề bản chất là các phiên bản lồng nhau củalet , vì vậy sử dụng một trong hai này sẽ đắt hơn. Việc chấm điểm này cho phép bạn quản lý vi mô việc tối ưu hóa chương trình của mình vì bạn có thể chọn mức cho phép bạn cần cho nhiệm vụ hiện tại. Nếu bạn không cần đệ quy hoặc khả năng tham chiếu đến các ràng buộc trước đó, thì bạn có thể quay lại một lệnh đơn giản để tiết kiệm một chút hiệu suất.

Nó tương tự như các vị từ bình đẳng được phân loại trong Đề án. (tức là eq?, eqv?equal?)

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.