Làm thế nào những người đề xuất lập trình chức năng trả lời tuyên bố này trong Code Complete?


30

Trên trang 839 của phiên bản thứ hai, Steve McConnell đang thảo luận về tất cả các cách mà các lập trình viên có thể "chinh phục sự phức tạp" trong các chương trình lớn. Lời khuyên của ông lên đến đỉnh điểm với tuyên bố này:

"Lập trình hướng đối tượng cung cấp một mức độ trừu tượng áp dụng cho các thuật toán và dữ liệu cùng một lúc , một loại trừu tượng mà phân rã chức năng đơn thuần không cung cấp."

Cùng với kết luận của mình rằng "giảm độ phức tạp được cho là chìa khóa quan trọng nhất để trở thành một lập trình viên hiệu quả" (cùng một trang), điều này dường như là một thách thức đối với lập trình chức năng.

Cuộc tranh luận giữa FP và OO thường được đóng khung bởi những người đề xuất FP xung quanh các vấn đề phức tạp xuất phát từ những thách thức của sự tương tranh hoặc song song. Nhưng đồng thời chắc chắn không phải là loại lập trình viên phần mềm phức tạp duy nhất cần chinh phục. Có lẽ tập trung vào việc giảm một loại phức tạp làm tăng nó rất nhiều trong các khía cạnh khác, như vậy trong nhiều trường hợp, mức tăng không đáng giá.

Nếu chúng ta chuyển các điều khoản so sánh giữa FP và OO từ các vấn đề cụ thể như đồng thời hoặc tái sử dụng sang quản lý sự phức tạp toàn cầu, cuộc tranh luận đó sẽ như thế nào?

CHỈNH SỬA

Sự tương phản tôi muốn nhấn mạnh là OO dường như gói gọn và trừu tượng khỏi sự phức tạp của cả dữ liệu và thuật toán, trong khi lập trình chức năng dường như khuyến khích để các chi tiết triển khai của cấu trúc dữ liệu "lộ" hơn trong suốt chương trình.

Xem, ví dụ, Stuart Halloway (một người ủng hộ Clojure FP) ở đây nói rằng "quá đặc điểm kỹ thuật của các kiểu dữ liệu" là "hậu quả tiêu cực của phong cách OO thành ngữ" và ưu khái niệm một AddressBook như một vector đơn giản hay bản đồ thay vì một đối tượng OO phong phú hơn với các thuộc tính và phương thức bổ sung (không vectơ & không ánh xạ). .


3
+1 mặc dù câu hỏi đã được đóng khung khá đối nghịch, đó là một câu hỏi hay.
mattnz

16
Như nhiều người đã nêu trong các câu trả lời, Phân rã chức năng và lập trình chức năng là hai con thú khác nhau. Vì vậy, kết luận rằng "điều này dường như khá nhiều thách thức đối với lập trình chức năng" là hoàn toàn sai, nó không liên quan gì đến nó.
Fabio Fracassi

5
Rõ ràng kiến ​​thức của McConnel trong các hệ thống kiểu dữ liệu chức năng hiện đại và các mô-đun hạng nhất có thứ tự cao là hơi chắp vá. Tuyên bố của ông hoàn toàn vô nghĩa, vì chúng tôi đã có các mô-đun và hàm xử lý lớp đầu tiên (xem SML), loại lớp (xem Haskell). Đó chỉ là một ví dụ khác về cách suy nghĩ của OO là một tôn giáo hơn là một phương pháp thiết kế tôn trọng. Và, nhân tiện, bạn lấy thứ này ở đâu về sự tương tranh? Hầu hết các lập trình viên chức năng hoàn toàn không quan tâm đến sự song song.
SK-logic

6
@ SK-logic Tất cả McConnell nói là "phân rã chức năng một mình" không cung cấp cùng một phương tiện trừu tượng như OOP, có vẻ như là một tuyên bố khá an toàn với tôi. Không nơi nào ông nói rằng các ngôn ngữ FP không có phương tiện trừu tượng mạnh như OOP. Trong thực tế, ông không đề cập đến ngôn ngữ FP. Đó chỉ là cách giải thích của OP.
sepp2k

2
@ sepp2k, ok, tôi hiểu rồi. Tuy nhiên, một hệ thống cấu trúc dữ liệu rất phức tạp và được sắp xếp hợp lý có thể được xây dựng dựa trên sự phân rã chức năng cho phép tính lambda gần như thuần túy - thông qua mô phỏng hành vi của các mô-đun. Không cần trừu tượng OO cả.
SK-logic

Câu trả lời:


13

Hãy nhớ rằng cuốn sách đã được viết hơn 20 năm đi. Đối với các lập trình viên chuyên nghiệp thời đó, FP không tồn tại - nó hoàn toàn nằm trong địa hạt của các học giả & nhà nghiên cứu.

Chúng ta cần đóng khung "phân rã chức năng" trong bối cảnh thích hợp của công việc. Tác giả không đề cập đến lập trình chức năng. Chúng ta cần buộc điều này trở lại với "lập trình có cấu trúc" và GOTOmớ hỗn độn đã xuất hiện trước nó. Nếu điểm tham chiếu của bạn là FORTRAN / COBOL / BASIC cũ không có chức năng (có thể, nếu bạn may mắn, bạn sẽ nhận được một cấp GOSUB duy nhất) và tất cả các biến của bạn là toàn cầu, có thể phá vỡ chương trình của bạn thành các lớp chức năng là một lợi ích lớn.

OOP là một sàng lọc tiếp theo về loại 'phân rã chức năng' này. Bạn không chỉ có thể bó các hướng dẫn với nhau trong các chức năng mà bạn còn có thể nhóm các chức năng liên quan với dữ liệu họ đang làm việc. Kết quả là một đoạn mã được xác định rõ ràng mà bạn có thể nhìn và hiểu (lý tưởng) mà không cần phải theo đuổi xung quanh cơ sở mã của bạn để tìm những gì khác có thể hoạt động trên dữ liệu của bạn.


27

Tôi tưởng tượng những người đề xướng lập trình chức năng sẽ lập luận rằng hầu hết các ngôn ngữ FP cung cấp nhiều phương tiện trừu tượng hơn là "phân rã chức năng một mình" và thực tế cho phép các phương tiện trừu tượng có thể so sánh với các ngôn ngữ hướng đối tượng. Ví dụ, người ta có thể trích dẫn các lớp loại của Haskell hoặc các mô đun bậc cao hơn của ML như các phương tiện trừu tượng như vậy. Do đó, tuyên bố (mà tôi khá chắc chắn là về định hướng đối tượng so với lập trình thủ tục, không phải lập trình chức năng) không áp dụng cho chúng.

Cũng cần chỉ ra rằng FP và OOP là các khái niệm trực giao và không loại trừ lẫn nhau. Vì vậy, không có nghĩa gì khi so sánh chúng với nhau. Bạn rất có thể so sánh "OOP bắt buộc" (ví dụ Java) và "OOP chức năng" (ví dụ Scala), nhưng câu lệnh bạn trích dẫn sẽ không áp dụng cho so sánh đó.


10
+1 "Phân rã chức năng"! = "Lập trình chức năng". Việc đầu tiên dựa trên mã hóa tuần tự cổ điển bằng cách sử dụng các cấu trúc dữ liệu vanilla không có sự kế thừa, đóng gói & đa hình. Thứ hai thể hiện các giải pháp sử dụng phép tính lambda. Hai điều hoàn toàn khác nhau.
Nhị phân nhị phân

4
Xin lỗi, nhưng cụm từ "lập trình thủ tục" ngoan cố từ chối đến với tâm trí sớm hơn. "Phân rã chức năng" đối với tôi là chỉ dẫn nhiều hơn về lập trình thủ tục so với lập trình chức năng.
Nhị phân nhị phân

Vâng bạn đã đúng. Tôi đã giả định rằng Lập trình chức năng ưu tiên các chức năng có thể sử dụng lại hoạt động trên cùng một cấu trúc dữ liệu đơn giản (danh sách, cây, bản đồ) và thực sự tuyên bố rằng đây là điểm bán hàng trên OO. Xem Stuart Halloway (một người đề xuất Clojure FP) ở đây nói rằng "đặc tả quá mức của các loại dữ liệu" là "hậu quả tiêu cực của kiểu OO thành ngữ" và ưu tiên khái niệm một Sổ địa chỉ như một vectơ hoặc bản đồ thay vì một đối tượng OO phong phú hơn Các thuộc tính và phương thức -vectorish & không ánh xạ).
dan

Liên kết cho trích dẫn Stuart Halloway: thinkrelevance.com/blog/2009/08/12/ Khăn
dan

2
@dan Đó có thể là cách nó được thực hiện bằng ngôn ngữ được gõ động Clojure (Tôi không biết, tôi không sử dụng Clojure), nhưng tôi nghĩ thật nguy hiểm khi kết luận từ đó, đó là cách nó được thực hiện trong FP nói chung. Ví dụ, người Haskell dường như rất lớn về các kiểu trừu tượng và ẩn thông tin (có lẽ không nhiều như người Java).
sepp2k

7

Tôi thấy lập trình chức năng cực kỳ hữu ích trong việc quản lý sự phức tạp. Bạn có xu hướng suy nghĩ về sự phức tạp theo một cách khác, xác định nó là các hàm hoạt động trên dữ liệu bất biến ở các cấp độ khác nhau thay vì đóng gói theo nghĩa OOP.

Ví dụ, gần đây tôi đã viết một trò chơi trong Clojure và toàn bộ trạng thái của trò chơi được xác định trong một cấu trúc dữ liệu bất biến duy nhất:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

Và vòng lặp trò chơi chính có thể được định nghĩa là áp dụng một số chức năng thuần túy cho trạng thái trò chơi trong một vòng lặp:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

Hàm chính được gọi là update-game, chạy một bước mô phỏng với trạng thái trò chơi trước đó và một số đầu vào của người dùng và trả về trạng thái trò chơi mới.

Vậy sự phức tạp ở đâu? Theo quan điểm của tôi, nó đã được quản lý khá tốt:

  • Chắc chắn chức năng cập nhật trò chơi thực hiện rất nhiều công việc, nhưng nó được xây dựng bằng cách soạn thảo các chức năng khác để nó thực sự là một công việc khá đơn giản. Khi bạn xuống một vài cấp độ, các chức năng vẫn khá đơn giản, thực hiện một số việc như "thêm một đối tượng vào ô bản đồ".
  • Chắc chắn trạng thái trò chơi là một cấu trúc dữ liệu lớn. Nhưng một lần nữa, nó chỉ được xây dựng bằng cách soạn các cấu trúc dữ liệu cấp thấp hơn. Ngoài ra, đó là "dữ liệu thuần túy" thay vì yêu cầu bất kỳ phương thức nào được nhúng hoặc định nghĩa lớp (bạn có thể nghĩ về nó như một đối tượng JSON bất biến rất hiệu quả nếu bạn muốn) vì vậy có rất ít bản tóm tắt.

OOP cũng có thể quản lý sự phức tạp thông qua đóng gói, nhưng nếu bạn so sánh điều này với OOP, chức năng đã tiếp cận một số lợi thế rất lớn:

  • Cấu trúc dữ liệu trạng thái trò chơi là bất biến, vì vậy rất nhiều xử lý có thể dễ dàng được thực hiện song song. Ví dụ: hoàn toàn an toàn khi có màn hình vẽ kết xuất cuộc gọi trong một luồng khác với logic trò chơi - chúng không thể ảnh hưởng lẫn nhau hoặc nhìn thấy trạng thái không nhất quán. Điều này thật khó khăn với một đồ thị đối tượng có thể thay đổi lớn ......
  • Bạn có thể chụp ảnh trạng thái trò chơi bất cứ lúc nào. Các câu trả lời là không đáng kể (bất kỳ nhờ vào cấu trúc dữ liệu liên tục của Clojure, các bản sao hầu như không chiếm bất kỳ bộ nhớ nào vì hầu hết các dữ liệu được chia sẻ). Bạn cũng có thể chạy trò chơi cập nhật để "dự đoán tương lai" để giúp AI đánh giá các bước đi khác nhau chẳng hạn.
  • Không nơi nào tôi phải thực hiện bất kỳ sự đánh đổi khó khăn nào để phù hợp với mô hình OOP, chẳng hạn như xác định một chế độ thừa kế giai cấp cứng nhắc. Theo nghĩa này, cấu trúc dữ liệu chức năng hoạt động giống như một hệ thống dựa trên nguyên mẫu linh hoạt.

Cuối cùng, đối với những người quan tâm đến những hiểu biết sâu sắc hơn về cách quản lý sự phức tạp trong các ngôn ngữ chức năng so với ngôn ngữ OOP, tôi mạnh mẽ giới thiệu lại video bài phát biểu quan trọng của Rich Hickey Simple Made Easy (quay tại hội nghị công nghệ Strange Loop )


2
Tôi nghĩ rằng một trò chơi là một trong những ví dụ tồi tệ nhất có thể để chứng minh "lợi ích" được cho là của tính bất biến được thi hành. Mọi thứ liên tục di chuyển trong một trò chơi, điều đó có nghĩa là bạn phải xây dựng lại trạng thái trò chơi của mình mọi lúc. Và nếu mọi thứ đều bất biến, điều đó có nghĩa là bạn không chỉ phải xây dựng lại trạng thái trò chơi, mà tất cả mọi thứ trong biểu đồ có tham chiếu đến nó, hoặc có tham chiếu đến điều đó, và cứ thế đệ quy cho đến khi bạn tái chế toàn bộ chương trình ở mức 30+ FPS, với hàng tấn churn để khởi động! Không có cách nào bạn có được hiệu suất tốt từ đó ...
Mason Wheeler

7
Tất nhiên các trò chơi rất khó với tính bất biến - đó là lý do tại sao tôi chọn nó để chứng minh rằng nó vẫn có thể hoạt động! Tuy nhiên, bạn sẽ ngạc nhiên về những gì cấu trúc dữ liệu liên tục có thể làm - hầu hết trạng thái trò chơi không cần phải được xây dựng lại, chỉ có những thứ thay đổi. Và chắc chắn có một số chi phí, nhưng đó chỉ là một yếu tố không đổi nhỏ. Cung cấp cho tôi đủ số lõi và tôi sẽ đánh bại công cụ trò chơi C ++ đơn luồng của bạn .....
mikera

3
@Mason Wheeler: Thực ra, nó tốt để có được hầu như bằng nhau (tốt như với đột biến) thực hiện với các đối tượng bất biến, mà không có nhiều GC ở tất cả. Thủ thuật trong Clojure là sử dụng các cấu trúc dữ liệu liên tục : chúng là bất biến đối với lập trình viên, nhưng thực sự có thể thay đổi dưới mui xe. Tốt nhất của cả hai thế giới.
Joonas Pulakka

4
@quant_dev Nhiều lõi rẻ hơn lõi tốt hơn ... escapistmagazine.com/news/view/
mẹo

6
@quant_dev - đó không phải là một cái cớ, đó là một sự thật về mặt kiến ​​trúc và toán học rằng bạn nên có một chi phí phát sinh liên tục nếu bạn có thể bù đắp bằng cách tăng hiệu suất của bạn gần tuyến tính với số lượng lõi. Lý do các ngôn ngữ chức năng cuối cùng sẽ cung cấp hiệu suất vượt trội là vì chúng ta đã đi đến cuối dòng cho hiệu suất lõi đơn, và tất cả sẽ là về sự tương tranh và song song trong tương lai. phương pháp tiếp cận chức năng (và đặc biệt là bất biến) rất quan trọng trong việc thực hiện công việc này.
mikera

3

"Lập trình hướng đối tượng cung cấp một mức độ trừu tượng áp dụng cho các thuật toán và dữ liệu cùng một lúc, một loại trừu tượng mà phân rã chức năng đơn thuần không cung cấp."

Phân rã chức năng thôi không đủ để tạo ra bất kỳ loại thuật toán hoặc chương trình nào: bạn cũng cần phải trình bày dữ liệu. Tôi nghĩ rằng tuyên bố trên mặc nhiên giả định (hoặc ít nhất có thể hiểu như thế) rằng "dữ liệu" trong trường hợp chức năng thuộc loại thô sơ nhất: chỉ liệt kê các biểu tượng và không có gì khác. Lập trình trong một ngôn ngữ như vậy rõ ràng là không thuận tiện. Tuy nhiên, nhiều ngôn ngữ, đặc biệt là các ngôn ngữ mới và hiện đại, chức năng (hoặc đa âm), như Clojure, cung cấp các cấu trúc dữ liệu phong phú: không chỉ liệt kê, mà còn cả chuỗi, vectơ, bản đồ và bộ, bản ghi, cấu trúc - và đối tượng! - với siêu dữ liệu và đa hình.

Thành công thực tế lớn của trừu tượng OO khó có thể bị tranh cãi. Nhưng đó có phải là từ cuối cùng? Như bạn đã viết, các vấn đề đồng thời đã là nỗi đau lớn và OO cổ điển không có ý tưởng nào về sự tương tranh cả. Kết quả là, các giải pháp OO thực tế để xử lý đồng thời chỉ là băng keo chồng chất: hoạt động, nhưng dễ dàng vặn vẹo, lấy đi một lượng đáng kể tài nguyên não từ nhiệm vụ thiết yếu trong tay và nó không có quy mô tốt. Có lẽ nó có thể tận dụng tốt nhất của nhiều thế giới. Đó là những gì các ngôn ngữ đa ngôn ngữ hiện đại đang theo đuổi.


1
Tôi đã nghe câu "OO in the Large, FP in the small" ở đâu đó - tôi nghĩ Michael Feathers đã trích dẫn nó. Có nghĩa là, FP đó có thể tốt cho các phần cụ thể của một chương trình lớn, nhưng nói chung, nó phải là OO.
dan

Ngoài ra, thay vì sử dụng Clojure cho mọi thứ, ngay cả những thứ được thể hiện rõ ràng hơn theo cú pháp OO truyền thống hơn, làm thế nào về việc sử dụng Clojure cho các bit xử lý dữ liệu nơi nó sạch hơn và sử dụng Java hoặc một số ngôn ngữ OO khác cho các bit khác. Lập trình polyglot thay vì lập trình đa hướng với cùng một ngôn ngữ cho tất cả các phần của chương trình. (Sắp xếp giống như cách mà hầu hết các ứng dụng web sử dụng SQL và OO cho các lớp khác nhau.)?
dan

@dan: Sử dụng bất cứ công cụ nào phù hợp với công việc nhất. Trong lập trình polyglot, yếu tố quan trọng là giao tiếp thuận tiện giữa các ngôn ngữ và Clojure và Java khó có thể chơi tốt hơn với nhau . Tôi tin rằng hầu hết các chương trình Clojure đáng kể đều sử dụng ít nhất một số bit của các thư viện Java tiêu chuẩn của JDK ở đây và đó.
Joonas Pulakka

2

Trạng thái có thể thay đổi là gốc rễ của hầu hết các vấn đề phức tạp và liên quan đến lập trình và thiết kế phần mềm / hệ thống.

OO bao trùm trạng thái đột biến. FP tác giả nhà nước đột biến.

Cả OO và FP đều có công dụng & điểm ngọt. Chọn một cách khôn ngoan. Và hãy nhớ câu ngạn ngữ: "Đóng cửa là đối tượng của người nghèo. Đối tượng là sự đóng cửa của người nghèo."


3
Tôi không chắc khẳng định mở đầu của bạn là đúng. Nguồn gốc của "hầu hết" của sự phức tạp? Trong chương trình tôi đã thực hiện hoặc đã thấy, vấn đề không phải là quá nhiều trạng thái có thể thay đổi là thiếu trừu tượng và quá nhiều chi tiết thông qua mã.
dan

1
@Dan: Thú vị. Thực tế, tôi đã thấy rất nhiều điều ngược lại: Các vấn đề bắt nguồn từ việc sử dụng quá mức trừu tượng, gây khó khăn cho cả việc hiểu và khi cần thiết để khắc phục các chi tiết về những gì đang thực sự xảy ra.
Mason Wheeler

1

Lập trình hàm có thể có các đối tượng, nhưng các đối tượng đó có xu hướng bất biến. Các hàm thuần túy (các hàm không có tác dụng phụ) sau đó hoạt động trên các cấu trúc dữ liệu đó. Có thể tạo các đối tượng bất biến trong các ngôn ngữ lập trình hướng đối tượng, nhưng chúng không được thiết kế để làm điều đó và đó không phải là cách chúng có xu hướng được sử dụng. Điều này làm cho nó khó để lý luận về các chương trình hướng đối tượng.

Hãy lấy một ví dụ rất đơn giản. Giả sử rằng Oracle đã quyết định rằng Chuỗi Java nên có một phương thức ngược lại và bạn đã viết đoạn mã sau.

String x = "abc";
StringBuffer y = new StringBuffer(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString());

dòng cuối cùng đánh giá là gì? Bạn cần có kiến ​​thức đặc biệt về lớp String để biết rằng điều này sẽ đánh giá thành sai.

Điều gì sẽ xảy ra nếu tôi tạo lớp WuHoString của riêng mình

String x = "abc";
WuHoString y = new WuHoString(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString())

Không thể biết dòng cuối cùng đánh giá là gì.

Trong một kiểu lập trình hàm, nó sẽ được viết nhiều hơn như sau:

String x;
equals(toString(reverse(x)), toString(reverse(WuHoString(x))))

và nó phải là sự thật

Nếu 1 hàm trong một trong những lớp cơ bản nhất rất khó để suy luận thì người ta tự hỏi liệu việc đưa ra ý tưởng về các đối tượng có thể thay đổi này đã làm tăng hay giảm độ phức tạp.

Rõ ràng có tất cả các loại định nghĩa về những gì cấu thành đối tượng hướng và ý nghĩa của chức năng và ý nghĩa của việc có cả hai. Đối với tôi, bạn có thể có một "phong cách lập trình chức năng" trong sự uể oải, không có những thứ như các hàm hạng nhất nhưng các ngôn ngữ khác được tạo ra cho nó.


3
Thật buồn cười khi bạn nói rằng các ngôn ngữ OO không được xây dựng cho các đối tượng bất biến và sau đó sử dụng một ví dụ với các chuỗi (không thể thay đổi trong hầu hết các ngôn ngữ OO bao gồm cả Java). Ngoài ra tôi nên chỉ ra rằng có các ngôn ngữ OO (hay đúng hơn là nhiều mô hình) được thiết kế với sự nhấn mạnh vào các đối tượng bất biến (ví dụ Scala).
sepp2k

@ sepp2k: Làm quen với nó. Những người ủng hộ FP luôn ném xung quanh những ví dụ kỳ quái, giả tạo không liên quan gì đến tiền mã hóa trong thế giới thực. Đó là cách duy nhất để làm cho các khái niệm FP cốt lõi như tính bất biến được thi hành trông tốt.
Mason Wheeler

1
@Mason: Hả? Không phải cách tốt nhất để làm cho tính bất biến trở nên tốt đẹp là nói "Java (và C #, python, v.v.) sử dụng các chuỗi bất biến và nó hoạt động rất tốt"?
sepp2k

1
@ sepp2k: Nếu các chuỗi bất biến hoạt động rất tốt, tại sao các lớp kiểu StringBuilder / StringBuffer cứ hiển thị khắp nơi? Nó chỉ là một ví dụ khác về sự đảo ngược trừu tượng cản trở bạn.
Mason Wheeler

2
Nhiều ngôn ngữ hướng đối tượng cho phép bạn tạo các đối tượng bất biến. Nhưng khái niệm buộc các phương thức vào lớp thực sự không khuyến khích nó theo quan điểm của tôi. Ví dụ String không thực sự là một ví dụ giả định. Bất cứ khi nào tôi gọi bất kỳ mehtod nào trong java, tôi đều có cơ hội xem liệu các tham số của tôi sẽ bị thay đổi trong hàm đó hay không.
WuHoUnited

0

Tôi nghĩ rằng trong hầu hết các trường hợp, sự trừu tượng OOP cổ điển không bao hàm sự phức tạp đồng thời. Do đó, OOP (theo nghĩa gốc của nó) không loại trừ FP và đó là lý do tại sao chúng ta thấy những thứ như scala.


0

Câu trả lời phụ thuộc vào ngôn ngữ. Lisps, ví dụ, có mã thực sự gọn gàng mà mã dữ liệu - các thuật toán bạn viết thực ra chỉ là danh sách Lisp! Bạn lưu trữ dữ liệu giống như cách bạn viết chương trình. Sự trừu tượng này đồng thời đơn giản và kỹ lưỡng hơn OOP và cho phép bạn thực hiện một số điều thực sự gọn gàng (kiểm tra các macro).

Haskell (và ngôn ngữ tương tự, tôi tưởng tượng) có một câu trả lời hoàn toàn khác: các loại dữ liệu đại số. Một kiểu dữ liệu đại số giống như một Ccấu trúc, nhưng có nhiều tùy chọn hơn. Những kiểu dữ liệu này cung cấp sự trừu tượng cần thiết để mô hình hóa dữ liệu; các hàm cung cấp sự trừu tượng cần thiết để mô hình hóa các thuật toán. Các lớp học kiểu và các tính năng tiên tiến khác cung cấp một thậm chí cao hơn mức độ trừu tượng hơn cả.

Ví dụ, tôi đang làm việc trên một ngôn ngữ lập trình gọi là TPL cho vui. Các kiểu dữ liệu đại số làm cho nó thực sự dễ dàng để biểu diễn các giá trị:

data TPLValue = Null
              | Number Integer
              | String String
              | List [TPLValue]
              | Function [TPLValue] TPLValue
              -- There's more in the real code...

Điều này nói - theo một cách rất trực quan - là một TPLValue (bất kỳ giá trị trong ngôn ngữ của tôi) có thể là một Nullhoặc một Numbervới một Integergiá trị hoặc thậm chí một Functionvới một danh sách các giá trị (các thông số) và một giá trị cuối cùng (cơ thể ).

Tiếp theo tôi có thể sử dụng các lớp loại để mã hóa một số hành vi phổ biến. Ví dụ, tôi có thể tạo TPLValuevà ví dụ Showcó nghĩa là nó có thể được chuyển đổi thành một chuỗi.

Ngoài ra, tôi có thể sử dụng các lớp loại của riêng mình khi tôi cần chỉ định hành vi của một số loại nhất định (bao gồm cả các loại mà tôi không tự thực hiện). Ví dụ, tôi có một Extractablelớp loại cho phép tôi viết một hàm lấy một TPLValuevà trả về một giá trị bình thường thích hợp. Như vậy extractcó thể chuyển đổi một Numberđến một Integerhoặc một Stringđến một Stringchừng nào IntegerStringlà trường hợp của Extractable.

Cuối cùng, logic chính của chương trình của tôi là trong một số chức năng như evalapply. Đây thực sự là cốt lõi - họ lấy TPLValues và biến chúng thành nhiều TPLValues hơn , cũng như xử lý trạng thái và lỗi.

Nhìn chung, trừu tượng Tôi đang sử dụng trong mã Haskell tôi đang thực sự hơn mạnh mẽ hơn những gì tôi có thể đã được sử dụng trong một ngôn ngữ OOP.


Vâng, phải yêu eval. "Này, nhìn tôi này! Tôi không cần phải viết các lỗ hổng bảo mật của riêng mình; Tôi đã có một lỗ hổng thực thi mã tùy ý được xây dựng ngay trong ngôn ngữ lập trình!" Kết hợp dữ liệu với mã là nguyên nhân gốc rễ của một trong hai loại lỗ hổng bảo mật phổ biến nhất mọi thời đại. Bất cứ khi nào bạn thấy ai đó bị hack vì một cuộc tấn công SQL SQL (trong số nhiều thứ khác) là do một số lập trình viên ngoài kia không biết cách tách dữ liệu khỏi mã một cách chính xác.
Mason Wheeler

evalkhông phụ thuộc nhiều vào cấu trúc của Lisp - bạn có thể có các evalngôn ngữ như JavaScript và Python. Sức mạnh thực sự đến từ việc viết các macro, về cơ bản là các chương trình hoạt động trên các chương trình như dữ liệu và xuất các chương trình khác. Điều này làm cho ngôn ngữ rất linh hoạt và tạo ra sự trừu tượng mạnh mẽ dễ dàng.
Tikhon Jelvis

3
Vâng, tôi đã nghe nói "macro là tuyệt vời" nhiều lần trước đây. Nhưng tôi chưa bao giờ thấy một ví dụ thực tế nào về macro Lisp đang làm điều gì đó 1) là thực tế và điều gì đó bạn thực sự quan tâm để làm trong mã thế giới thực và 2) không thể được thực hiện dễ dàng như bất kỳ ngôn ngữ nào hỗ trợ các chức năng.
Mason Wheeler

1
@MasonWheeler ngắn mạch and. đoản mạch or. let. let-rec. cond. defn. Không ai trong số này có thể được thực hiện với các chức năng trong các ngôn ngữ đặt hàng ứng dụng. for(danh sách hiểu). dotimes. doto.

1
@MattFenwick: OK, tôi thực sự nên thêm một điểm thứ ba vào hai điểm trên của tôi: 3) chưa được tích hợp sẵn cho bất kỳ ngôn ngữ lập trình lành mạnh nào. Bởi vì đó là những ví dụ vĩ mô thực sự hữu ích duy nhất tôi từng thấy và khi bạn nói "Này, hãy nhìn tôi, ngôn ngữ của tôi linh hoạt đến mức tôi có thể thực hiện ngắn mạch của chính mình and!" Tôi nghe, "hey nhìn tôi, ngôn ngữ của tôi như vậy là làm tê liệt rằng nó thậm chí không đi kèm với một đoạn ngắn mạch andvà tôi phải phát minh lại bánh xe cho tất cả mọi thứ !"
Mason Wheeler

0

Câu trích dẫn không còn giá trị nữa, theo như tôi có thể thấy.

Các ngôn ngữ OO đương đại không thể trừu tượng hóa các loại có loại không *, tức là các loại loại cao hơn không xác định. Hệ thống loại của họ không cho phép thể hiện ý tưởng "một số vùng chứa các phần tử Int, cho phép ánh xạ một hàm qua các phần tử".

Do đó, chức năng cơ bản này như Haskells

fmap :: Functor f => (a -> b) -> f a -> f b 

không thể được viết dễ dàng bằng Java *), ví dụ, ít nhất là không theo cách an toàn. Do đó, để có được chức năng cơ bản, bạn phải viết nhiều bản tóm tắt, bởi vì bạn cần

  1. một phương pháp để áp dụng một hàm đơn giản cho các phần tử của danh sách
  2. một phương thức để áp dụng cùng một hàm đơn giản cho các phần tử của một mảng
  3. một phương thức để áp dụng cùng một hàm đơn giản cho các giá trị của hàm băm,
  4. .... bộ
  5. .... cây
  6. ... 10. kiểm tra đơn vị cho cùng

Tuy nhiên, năm phương thức đó về cơ bản là cùng một mã, cho hoặc lấy một số. Ngược lại, trong Haskell, tôi cần:

  1. Một phiên bản Functor cho danh sách, mảng, bản đồ, tập hợp và cây (chủ yếu được xác định trước hoặc có thể được trình biên dịch tự động)
  2. chức năng đơn giản

Lưu ý rằng điều này sẽ không thay đổi với Java 8 (chỉ là người ta có thể áp dụng các hàm dễ dàng hơn, nhưng sau đó, chính xác, vấn đề ở trên sẽ thành hiện thực. Miễn là bạn thậm chí không có các hàm bậc cao hơn, rất có thể bạn thậm chí không có thể hiểu những loại loại cao hơn là tốt cho.)

Ngay cả các ngôn ngữ OO mới như Ceylon cũng không có loại tốt hơn. (Gần đây tôi đã hỏi Gavin King và anh ấy nói với tôi, điều đó không quan trọng vào thời điểm này.) Tuy nhiên, đừng biết về Kotlin.

*) Để công bằng, bạn có thể có một Functor giao diện có fmap phương thức. Điều tồi tệ là, bạn không thể nói: Này, tôi biết cách triển khai fmap cho lớp thư viện SuperConcienBlockedDoublyLinkedDequeHasMap, trình biên dịch thân yêu, xin vui lòng chấp nhận rằng từ bây giờ, tất cả SuperConcienBlockedDoublyLinkedDequeHasMaps đều là Functor.


FTR: các typechecker Ceylon và JavaScript phụ trợ tại làm hỗ trợ cao tay loại (và cũng cao hơn cấp bậc loại). Nó được coi là một tính năng "thử nghiệm". Tuy nhiên, cộng đồng của chúng tôi đã đấu tranh để tìm các ứng dụng thực tế cho chức năng này, vì vậy đây là một câu hỏi mở cho dù đây sẽ là một phần "chính thức" của ngôn ngữ. Tôi làm hy vọng nó sẽ được hỗ trợ bởi các Java backend tại một số sân khấu.
Vua Gavin

-2

Bất cứ ai từng lập trình theo dBase đều sẽ biết các macro dòng đơn hữu ích như thế nào để tạo mã có thể sử dụng lại. Mặc dù tôi chưa lập trình ở Lisp, tôi đã đọc từ nhiều người khác, những người thề bằng cách biên dịch các macro thời gian. Ý tưởng tiêm mã vào mã của bạn tại thời gian biên dịch được sử dụng ở dạng đơn giản trong mọi chương trình C với lệnh "bao gồm". Bởi vì Lisp có thể làm điều này với chương trình Lisp và vì Lisp có tính phản xạ cao, nên bạn có thể linh hoạt hơn rất nhiều.

Bất kỳ lập trình viên nào chỉ cần lấy một chuỗi văn bản tùy ý từ web và chuyển nó vào cơ sở dữ liệu của họ không phải là một lập trình viên. Tương tự như vậy, bất kỳ ai cho phép dữ liệu "người dùng" tự động trở thành mã thực thi rõ ràng là ngu ngốc. Điều đó không có nghĩa là cho phép các chương trình thao tác dữ liệu tại thời điểm thực thi và sau đó thực thi nó dưới dạng mã là một ý tưởng tồi. Tôi tin rằng kỹ thuật này sẽ không thể thiếu trong tương lai sẽ có mã "thông minh" thực sự viết hầu hết các chương trình. Toàn bộ "vấn đề dữ liệu / mã" hay không là vấn đề bảo mật trong ngôn ngữ.

Một trong những vấn đề với hầu hết các ngôn ngữ là chúng được tạo ra cho một người ngoại tuyến duy nhất để thực thi một số chức năng cho chính họ. Các chương trình trong thế giới thực đòi hỏi nhiều người phải truy cập mọi lúc và cùng lúc từ nhiều Lõi và nhiều cụm máy tính. Bảo mật nên là một phần của ngôn ngữ chứ không phải là hệ điều hành và trong tương lai không xa nó sẽ như vậy.


2
Chào mừng đến với lập trình viên. Vui lòng xem xét giảm bớt các biện pháp tu từ trong câu trả lời của bạn và ủng hộ một số yêu cầu của bạn với các tài liệu tham khảo bên ngoài.

1
Bất kỳ lập trình viên nào cho phép dữ liệu người dùng tự động trở thành mã thực thi rõ ràng là không biết gì . Không ngu ngốc. Làm theo cách đó thường dễ dàng và rõ ràng, và nếu họ không biết tại sao đó là một ý tưởng tồi và rằng một giải pháp tốt hơn tồn tại, bạn thực sự không thể đổ lỗi cho họ vì đã làm điều đó. (Bất cứ ai tiếp tục làm như vậy sau khi họ được dạy rằng có một cách tốt hơn, tuy nhiên, rõ ràng là ngu ngốc.)
Mason Wheeler
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.