Các ứng dụng chức năng. Chức năng của các đơn đặt hàng cao hơn. Xem “Ngôn ngữ lập trình chức năng” là gì trong các từ điển khác

Lập trình hàm kết hợp các cách tiếp cận khác nhau để xác định các quy trình tính toán dựa trên các khái niệm trừu tượng và phương pháp xử lý dữ liệu tượng trưng khá chặt chẽ.

Một đặc điểm của ngôn ngữ lập trình hàm là văn bản chương trình trong ngôn ngữ lập trình hàm mô tả “cách giải quyết vấn đề”, nhưng không quy định trình tự hành động để giải quyết vấn đề đó. Các tính chất cơ bản ngôn ngữ chức năng lập trình: ngắn gọn, đơn giản, gõ mạnh, tính mô đun, sự hiện diện của các phép tính trì hoãn (lười biếng).

Các ngôn ngữ lập trình chức năng bao gồm: Lisp, Miranda, Gofel, ML, Standard ML, Objective CAML, F#, Scala, Pythagoras, v.v.

Ngôn ngữ lập trình thủ tục

Ngôn ngữ lập trình thủ tục cho phép người lập trình xác định từng bước trong quá trình giải quyết vấn đề. Điểm đặc biệt của các ngôn ngữ lập trình như vậy là các nhiệm vụ được chia thành các bước và giải quyết từng bước. Sử dụng ngôn ngữ thủ tục, lập trình viên xác định các cấu trúc ngôn ngữ để thực hiện một chuỗi các bước thuật toán.

Ngôn ngữ lập trình thủ tục: Ada, Basic, C, COBOL, Pascal, PL/1, Rapier, v.v.

Ngôn ngữ lập trình ngăn xếp

Ngôn ngữ lập trình ngăn xếp là ngôn ngữ lập trình sử dụng mô hình máy của ngăn xếp để truyền tham số. Các ngôn ngữ lập trình dựa trên ngăn xếp: Forth, PostScript, Java, C#, v.v. Khi sử dụng ngăn xếp làm kênh chính để truyền tham số giữa các từ, các thành phần ngôn ngữ sẽ tạo thành các cụm từ một cách tự nhiên (chuỗi tuần tự). Thuộc tính này đưa các ngôn ngữ này đến gần hơn với ngôn ngữ tự nhiên.

Ngôn ngữ lập trình hướng theo khía cạnh 5) Ngôn ngữ lập trình khai báo 6) Ngôn ngữ lập trình động 7) Ngôn ngữ lập trình giáo dục 8) Ngôn ngữ mô tả giao diện 9) Ngôn ngữ lập trình nguyên mẫu 10) Ngôn ngữ lập trình hướng đối tượng ​11) Ngôn ngữ lập trình logic 12) Ngôn ngữ lập trình kịch bản 13) Ngôn ngữ lập trình bí truyền


Tiêu chuẩn hóa ngôn ngữ lập trình. Mô hình lập trình

Khái niệm về ngôn ngữ lập trình gắn bó chặt chẽ với việc triển khai nó. Để đảm bảo rằng việc biên dịch cùng một chương trình bằng các trình biên dịch khác nhau luôn cho kết quả như nhau, các tiêu chuẩn ngôn ngữ lập trình đang được phát triển. Các tổ chức tiêu chuẩn hóa: Viện Tiêu chuẩn Quốc gia Hoa Kỳ ANSI, Viện Kỹ sư Điện và Điện tử IEEE, Tổ chức Tiêu chuẩn Quốc tế ISO.



Khi một ngôn ngữ được tạo ra, một tiêu chuẩn riêng sẽ được phát hành, do các nhà phát triển ngôn ngữ xác định. Nếu một ngôn ngữ trở nên phổ biến thì theo thời gian phiên bản khác nhau trình biên dịch không tuân thủ nghiêm ngặt một tiêu chuẩn riêng. Trong hầu hết các trường hợp, có sự mở rộng các khả năng cố định ban đầu của ngôn ngữ. Một tiêu chuẩn đồng thuận đang được phát triển để đưa các triển khai ngôn ngữ phổ biến nhất phù hợp với nhau. Một yếu tố rất quan trọng trong việc tiêu chuẩn hóa ngôn ngữ lập trình là tính kịp thời của sự xuất hiện của tiêu chuẩn - trước khi ngôn ngữ này trở nên phổ biến và nhiều cách triển khai không tương thích được tạo ra. Trong quá trình phát triển ngôn ngữ, những tiêu chuẩn mới có thể xuất hiện, phản ánh những đổi mới hiện đại.

Mô hình lập trình

Mô hình- một tập hợp các lý thuyết, tiêu chuẩn và phương pháp cùng thể hiện một cách tổ chức kiến ​​thức khoa học - nói cách khác là một cách nhìn thế giới. Bằng cách tương tự, người ta thường chấp nhận rằng mô hình trong lập trình là một cách khái niệm hóa xác định cách thực hiện các phép tính và cách cấu trúc và tổ chức công việc được thực hiện bởi máy tính.

Có một số mô hình lập trình cơ bản, trong đó quan trọng nhất hiện nay là các mô hình lập trình chỉ thị, hướng đối tượng và logic chức năng. Để hỗ trợ lập trình theo một mô hình cụ thể, các ngôn ngữ thuật toán đặc biệt đã được phát triển.

C và Pascal là ví dụ về các ngôn ngữ được thiết kế cho lập trình quy định, trong đó nhà phát triển chương trình sử dụng mô hình hướng quy trình, nghĩa là cố gắng tạo mã hoạt động trên dữ liệu theo cách thích hợp. Trong cách tiếp cận này, nguyên tắc hoạt động được coi là một chương trình (mã), phải thực hiện tất cả các hành động cần thiết trên dữ liệu thụ động để đạt được kết quả mong muốn.


Công nghệ lập trình là một quá trình phát triển các sản phẩm phần mềm được tạo ra như một tổng thể không thể tách rời dưới dạng các chương trình đã được thử nghiệm kỹ lưỡng và các tài liệu phương pháp luận mô tả mục đích và cách sử dụng chúng.

Lập trình- quá trình sáng tạo chương trình máy tính. Theo nghĩa rộng hơn: phạm vi các hoạt động gắn liền với việc tạo ra và duy trì công việc. trạng thái của chương trình - phần mềm máy tính.

Công nghệ lập trình- một tập hợp các phương pháp và công cụ được sử dụng trong quá trình phát triển phần mềm.

Công nghệ lập trình là tập hợp các chỉ dẫn công nghệ, bao gồm:

· chỉ dẫn trình tự các hoạt động công nghệ;

· liệt kê các điều kiện theo đó hoạt động này hoặc hoạt động kia được thực hiện;

· mô tả về các hoạt động, trong đó dữ liệu, kết quả ban đầu, cũng như các hướng dẫn, quy định, tiêu chuẩn, tiêu chí, v.v. được xác định cho mỗi hoạt động.

Công nghệ hiện đại lập trình - cách tiếp cận thành phần, bao gồm việc xây dựng phần mềm từ các thành phần riêng lẻ - các phần mềm riêng biệt về mặt vật lý tương tác với nhau thông qua giao diện nhị phân được tiêu chuẩn hóa. Hiện nay Tiêu chuẩn chất lượng một sản phẩm phần mềm được coi là:- chức năng; − độ tin cậy;− dễ sử dụng;− hiệu quả(tỷ lệ giữa mức độ dịch vụ do sản phẩm phần mềm cung cấp cho người dùng trong các điều kiện nhất định với khối lượng tài nguyên được sử dụng); khả năng bảo trì(các đặc điểm của sản phẩm phần mềm cho phép giảm thiểu nỗ lực thực hiện các thay đổi nhằm loại bỏ lỗi trong sản phẩm đó và sửa đổi sản phẩm đó cho phù hợp với nhu cầu thay đổi của người dùng); tính di động(khả năng một hệ thống phần mềm được chuyển từ môi trường này sang môi trường khác, đặc biệt là từ máy tính này sang máy tính khác).

Một giai đoạn quan trọng trong việc tạo ra một sản phẩm phần mềm là thử nghiệm và gỡ lỗi.

Gỡ lỗi− đây là hoạt động nhằm phát hiện và sửa lỗi trong sản phẩm phần mềm bằng cách sử dụng quy trình thực thi chương trình của nó.

Kiểm tra− đây là quá trình thực thi các chương trình của nó trên một tập hợp dữ liệu nhất định mà kết quả của ứng dụng đã được biết trước hoặc các quy tắc hoạt động của các chương trình này đã được biết trước.

Hiện có các phương pháp thử nghiệm PS sau đây:

1) Kiểm tra tĩnh - kiểm tra thủ công chương trình tại bàn.

2) Thử nghiệm xác định - với nhiều sự kết hợp khác nhau của dữ liệu nguồn.

3) Ngẫu nhiên – ref. Dữ liệu được chọn ngẫu nhiên và đầu ra được xác định bởi chất lượng của kết quả hoặc ước tính gần đúng.


Các phong cách lập trình.

Phong cách lập trình là tập hợp các kỹ thuật hoặc phương pháp lập trình mà người lập trình sử dụng để tạo ra các chương trình chính xác, hiệu quả, dễ sử dụng và dễ đọc.

Có một số phong cách lập trình:

  1. Lập trình thủ tục là lập trình trong đó chương trình là một chuỗi các câu lệnh. Được sử dụng trong các ngôn ngữ cấp cao Basic, Fortran, v.v.
  2. Lập trình chức năng là lập trình trong đó chương trình là một chuỗi các lệnh gọi hàm. Được sử dụng trong Lisp và các ngôn ngữ khác.
  3. Lập trình logic – đây là chương trình trong đó chương trình là một tập hợp các xác định mối quan hệ giữa các đối tượng. Được sử dụng trong Prolog và các ngôn ngữ khác.

Lập trình hướng đối tượng– đây là chương trình trong đó cơ sở của chương trình là một đối tượng là tập hợp dữ liệu và quy tắc để chuyển đổi chúng. Được sử dụng trong Turbo-Pascal, C++, v.v.

Tính toán lười biếng

TRONG ngôn ngữ truyền thống lập trình (ví dụ: C++) gọi một hàm sẽ khiến tất cả các đối số được đánh giá. Phương pháp gọi hàm này được gọi là gọi theo giá trị. Nếu bất kỳ đối số nào không được sử dụng trong hàm thì kết quả của phép tính sẽ bị mất, do đó, phép tính được thực hiện vô ích. Theo một nghĩa nào đó, đối lập với gọi theo giá trị là gọi theo nhu cầu. Trong trường hợp này, đối số chỉ được đánh giá nếu nó cần thiết để tính kết quả. Một ví dụ về hành vi này là toán tử kết hợp từ C++ (&&), toán tử này không đánh giá giá trị của đối số thứ hai nếu đối số thứ nhất là sai.

Nếu một ngôn ngữ chức năng không hỗ trợ đánh giá lười biếng thì nó được gọi là nghiêm ngặt. Trên thực tế, trong những ngôn ngữ như vậy, thứ tự đánh giá được xác định nghiêm ngặt. Ví dụ về các ngôn ngữ nghiêm ngặt bao gồm Đề án, ML tiêu chuẩn và Caml.

Các ngôn ngữ sử dụng đánh giá lười biếng được gọi là không nghiêm ngặt. Haskell là một ngôn ngữ lỏng lẻo, giống như Gofer và Miranda chẳng hạn. Ngôn ngữ lỏng lẻo thường thuần khiết.

Rất thường xuyên, các ngôn ngữ nghiêm ngặt bao gồm các phương tiện để hỗ trợ một số các tính năng hữu ích, vốn có trong các ngôn ngữ không nghiêm ngặt, chẳng hạn như danh sách vô hạn. TRONG Giao hàng tiêu chuẩn ML chứa một mô-đun đặc biệt để hỗ trợ tính toán hoãn lại. Ngoài ra, Objective Caml còn hỗ trợ thêm từ dành riêng cho lười biếng và cấu trúc cho danh sách các giá trị được tính toán khi cần.

Phần này cung cấp Mô tả ngắn một số ngôn ngữ lập trình hàm (rất ít).

§ nói ngọng(Danh sách bộ xử lý). Nó được coi là ngôn ngữ lập trình chức năng đầu tiên. Chưa được gõ. Nó chứa rất nhiều thuộc tính bắt buộc, nhưng nhìn chung nó khuyến khích phong cách lập trình chức năng. Sử dụng cuộc gọi theo giá trị để tính toán. Có một phương ngữ hướng đối tượng của ngôn ngữ - CLOS.

§ ISWIM(Nếu bạn hiểu ý tôi). Nguyên mẫu ngôn ngữ chức năng. Được Landin phát triển vào những năm 60 của thế kỷ 20 để chứng minh ngôn ngữ lập trình chức năng có thể là gì. Cùng với ngôn ngữ, Landin còn phát triển một máy ảo đặc biệt để thực thi các chương trình trên ISWIM. Máy ảo gọi theo giá trị này được gọi là máy SECD. Cú pháp của nhiều ngôn ngữ chức năng dựa trên cú pháp của ngôn ngữ ISWIM. Cú pháp của ISWIM tương tự như ML, đặc biệt là Caml.

§ Cơ chế. Một phương ngữ Lisp dành cho nghiên cứu khoa học trong lĩnh vực khoa học máy tính. Đề án được thiết kế tập trung vào sự sang trọng và đơn giản của ngôn ngữ. Điều này làm cho ngôn ngữ nhỏ hơn nhiều so với Common Lisp.


§ M.L.(Ngôn ngữ Meta). Một họ các ngôn ngữ nghiêm ngặt với hệ thống kiểu đa hình được phát triển và các mô-đun có thể tham số hóa. ML được giảng dạy ở nhiều trường đại học phương Tây (một số thậm chí còn là ngôn ngữ lập trình đầu tiên).

§ ML tiêu chuẩn. Một trong những ngôn ngữ lập trình hàm được gõ đầu tiên. Chứa một số thuộc tính bắt buộc như liên kết đến giá trị thay đổi và do đó không tinh khiết. Sử dụng cuộc gọi theo giá trị để tính toán. Một triển khai mô-đun rất thú vị. Hệ thống loại đa hình mạnh mẽ. Tiêu chuẩn ngôn ngữ mới nhất là Tiêu chuẩn ML-97, trong đó có các định nghĩa toán học chính thức về cú pháp, cũng như ngữ nghĩa tĩnh và động của ngôn ngữ.

§ Ánh sáng camCam khách quan. Giống như Standard ML, nó thuộc họ ML. Caml mục tiêu khác với Caml Light chủ yếu ở chỗ nó hỗ trợ lập trình hướng đối tượng cổ điển. Giống như Standard ML rất nghiêm ngặt nhưng có một số hỗ trợ tích hợp cho việc đánh giá lười biếng.

§ Miranda. Được phát triển bởi David Turner như một ngôn ngữ chức năng tiêu chuẩn sử dụng đánh giá lười biếng. Nó có một hệ thống kiểu đa hình nghiêm ngặt. Giống như ML, nó được dạy ở nhiều trường đại học. Cung cấp ảnh hưởng lớn cho các nhà phát triển ngôn ngữ Haskell.

§ Haskell. Một trong những ngôn ngữ không nghiêm ngặt phổ biến nhất. Nó có một hệ thống đánh máy rất phát triển. Hệ thống mô-đun có phần kém phát triển hơn. Tiêu chuẩn ngôn ngữ mới nhất là Haskell-98.

§ Gofer(TỐT cho lý luận phương trình). Phương ngữ Haskell đơn giản hóa. Được thiết kế để dạy lập trình chức năng.

§ Lau dọn. Được thiết kế đặc biệt cho lập trình song song và phân tán. Cú pháp tương tự như Haskell. Lau dọn. Sử dụng tính toán hoãn lại. Trình biên dịch đi kèm với một bộ thư viện (thư viện I/O) cho phép bạn lập trình đồ họa giao diện người dùng trong Win32 hoặc MacOS.

Hãy nhớ lại rằng đặc điểm quan trọng nhất của phương pháp tiếp cận hàm là thực tế là bất kỳ chương trình nào được phát triển bằng ngôn ngữ lập trình hàm đều có thể được coi là một hàm, các đối số của nó cũng có thể là các hàm.

Cách tiếp cận chức năng đã tạo ra cả một họ ngôn ngữ, tổ tiên của ngôn ngữ đó, như đã lưu ý, là ngôn ngữ lập trình LISP. Sau đó, vào những năm 70, phiên bản gốc của ngôn ngữ ML đã được phát triển, đặc biệt là sau đó được phát triển thành SML, cũng như một số ngôn ngữ khác. Trong số này, có lẽ “trẻ nhất” là ngôn ngữ Haskell, được tạo ra khá gần đây, vào những năm 90.

Một lợi thế quan trọng của việc triển khai các ngôn ngữ lập trình hàm là tính năng phân bổ động tự động của bộ nhớ máy tính để lưu trữ dữ liệu. Trong trường hợp này, lập trình viên loại bỏ nhu cầu kiểm soát dữ liệu và nếu cần, có thể chạy chức năng "thu gom rác" - xóa bộ nhớ khỏi dữ liệu mà chương trình sẽ không cần nữa.

Các chương trình phức tạp với cách tiếp cận chức năngđược xây dựng bằng các hàm tổng hợp. Trong trường hợp này, văn bản chương trình là một hàm, một số đối số của nó cũng có thể được coi là hàm. Do đó, việc tái sử dụng mã có nghĩa là gọi một hàm được mô tả trước đó, cấu trúc của hàm này, không giống như một thủ tục ngôn ngữ mệnh lệnh, là minh bạch về mặt toán học.

Bởi vì hàm là một hình thức tự nhiên đối với các ngôn ngữ lập trình hàm, nên việc triển khai các khía cạnh khác nhau của lập trình liên quan đến hàm được đơn giản hóa rất nhiều. Viết trở nên minh bạch trực quan hàm đệ quy, I E. các hàm tự gọi mình là một đối số. Việc thực hiện xử lý cấu trúc dữ liệu đệ quy cũng trở nên tự nhiên.

Do thực hiện cơ chế khớp mẫu, các ngôn ngữ lập trình chức năng như ML và Haskell rất tốt cho việc xử lý biểu tượng.

Đương nhiên, các ngôn ngữ lập trình hàm không phải không có một số nhược điểm.

Những vấn đề này thường bao gồm cấu trúc phi tuyến tính của chương trình và hiệu quả thực hiện tương đối thấp. Tuy nhiên, nhược điểm đầu tiên khá chủ quan và nhược điểm thứ hai đã được khắc phục thành công nhờ các triển khai hiện đại, đặc biệt là một số trình dịch ngôn ngữ SML gần đây, bao gồm cả trình biên dịch cho môi trường Microsoft .NET.

Để phát triển phần mềm chuyên nghiệp bằng ngôn ngữ lập trình hàm, bạn cần hiểu sâu về bản chất của hàm.

Lưu ý rằng dưới thuật ngữ “hàm” trong cách hình thức hóa toán học và triển khai phần mềm có nghĩa là những khái niệm khác nhau.

Như vậy, hàm toán học f có miền định nghĩa A và dãy giá trị B là tập hợp các cặp có thứ tự

như vậy nếu

(a,b 1)f và (a,b 2)f,

Đổi lại, một hàm trong ngôn ngữ lập trình là một cấu trúc của ngôn ngữ này mô tả các quy tắc chuyển đổi một đối số (cái gọi là tham số thực tế) thành kết quả.

Để chính thức hóa khái niệm “hàm”, một lý thuyết toán học được gọi là phép tính lambda đã được xây dựng. Chính xác hơn, phép tính này nên được gọi là phép tính chuyển đổi lambda.

Chuyển đổi đề cập đến việc chuyển đổi các đối tượng tính toán (và trong lập trình, hàm và dữ liệu) từ dạng này sang dạng khác. Nhiệm vụ ban đầu trong toán học có mong muốn đơn giản hóa hình thức biểu thức. Trong lập trình, nhiệm vụ cụ thể này không quá quan trọng, mặc dù, như chúng ta sẽ thấy sau, việc sử dụng phép tính lambda làm công thức hóa ban đầu có thể giúp đơn giản hóa loại chương trình, tức là. dẫn đến tối ưu hóa mã chương trình.

Ngoài ra, các chuyển đổi cung cấp sự chuyển đổi sang các ký hiệu mới được giới thiệu và do đó, cho phép người ta biểu diễn lĩnh vực chủ đề ở dạng cô đọng hơn hoặc chi tiết hơn hoặc, bằng ngôn ngữ toán học, thay đổi mức độ trừu tượng liên quan đến lĩnh vực chủ đề. Tính năng này cũng được sử dụng rộng rãi bởi các ngôn ngữ lập trình hướng đối tượng và mô-đun có cấu trúc trong hệ thống phân cấp đối tượng, đoạn chương trình và cấu trúc dữ liệu. Sự tương tác của các thành phần ứng dụng trong .NET dựa trên cùng một nguyên tắc. Theo nghĩa này, việc chuyển đổi sang các ký hiệu mới là một trong những yếu tố quan trọng nhất của lập trình nói chung, và phép tính lambda (không giống như nhiều nhánh toán học khác) thể hiện một cách thích hợp để chính thức hóa các ký hiệu đổi mới.

Chúng ta hãy hệ thống hóa sự phát triển của các lý thuyết làm nền tảng cho cách tiếp cận hiện đại đối với phép tính lambda.

Hãy xem xét sự phát triển của các ngôn ngữ lập trình đang phát triển trong khuôn khổ cách tiếp cận chức năng.

Các ngôn ngữ lập trình chức năng ban đầu, bắt nguồn từ ngôn ngữ LISP (Xử lý LISt) cổ điển, được thiết kế để xử lý danh sách, tức là. thông tin tượng trưng. Trong trường hợp này, các loại chính là nguyên tố nguyên tử và danh sách các nguyên tố nguyên tử, và điểm nhấn chính là phân tích nội dung của danh sách.

Sự phát triển của các ngôn ngữ lập trình ban đầu đã trở thành ngôn ngữ lập trình chức năng với kiểu gõ mạnh mẽ, một ví dụ điển hình ở đây là ML cổ điển và SML hậu duệ trực tiếp của nó. Trong các ngôn ngữ có kiểu dữ liệu mạnh, mọi cấu trúc (hoặc biểu thức) đều phải có một kiểu.

Tuy nhiên, trong các ngôn ngữ lập trình chức năng sau này không cần gán kiểu rõ ràng và các loại biểu thức không được xác định ban đầu, như trong SML, có thể được suy ra (trước khi chương trình được chạy) dựa trên các loại biểu thức được liên kết với chúng .

Bước tiếp theo trong quá trình phát triển các ngôn ngữ lập trình hàm là hỗ trợ các hàm đa hình, tức là. hàm có đối số tham số (tương tự hàm toán học có tham số). Đặc biệt, tính đa hình được hỗ trợ trong SML, Miranda và Haskell.

Ở giai đoạn phát triển hiện nay, các ngôn ngữ lập trình chức năng “thế hệ mới” đã xuất hiện với các khả năng nâng cao sau: khớp mẫu (Scheme, SML, Miranda, Haskell), đa hình tham số (SML) và cái gọi là “lười biếng” (như cần thiết) tính toán (Haskell, Miranda, S.M.L.).

Nhóm ngôn ngữ lập trình chức năng khá lớn. Điều này được chứng minh không quá nhiều bởi danh sách ngôn ngữ quan trọng, mà bởi thực tế là nhiều ngôn ngữ đã tạo ra toàn bộ xu hướng trong lập trình. Chúng ta hãy nhớ lại rằng LISP đã tạo ra cả một nhóm ngôn ngữ: Lược đồ, InterLisp, LISP COMMON, v.v.

Ngôn ngữ lập trình SML cũng không ngoại lệ, được tạo ra dưới dạng ngôn ngữ ML bởi R. Milner tại MIT (Viện Công nghệ Massachusetts) và ban đầu được dùng để suy luận logic, đặc biệt là chứng minh định lý. Ngôn ngữ này được điển hình hóa nghiêm ngặt và thiếu tính đa hình tham số.

Sự phát triển của ML “cổ điển” đã trở thành ba ngôn ngữ hiện đại với các khả năng gần như giống hệt nhau (đa hình tham số, khớp mẫu, tính toán “lười biếng”). Đây là ngôn ngữ SML, được phát triển ở Anh và Mỹ, CaML, được tạo ra bởi một nhóm các nhà khoa học Pháp tại Viện INRIA, SML/NJ - một phương ngữ của SML từ New Jersey, và cũng là một sự phát triển của Nga - mosml (" Moscow" phương ngữ của ML).

Sự gần gũi với việc hình thức hóa toán học và định hướng hàm số ban đầu đã mang lại những ưu điểm sau của phương pháp tiếp cận hàm số:

1. dễ dàng kiểm tra và xác minh mã chương trình dựa trên khả năng xây dựng bằng chứng toán học nghiêm ngặt về tính đúng đắn của chương trình;

2. thống nhất cách trình bày chương trình và dữ liệu (dữ liệu có thể được gói gọn trong chương trình dưới dạng đối số hàm, việc chỉ định hoặc tính toán giá trị hàm có thể được thực hiện khi cần thiết);

3. gõ an toàn: loại trừ các hoạt động dữ liệu không hợp lệ;

4. gõ động: có thể phát hiện lỗi đánh máy trong thời gian chạy (việc thiếu thuộc tính này trong các ngôn ngữ lập trình chức năng đời đầu có thể dẫn đến RAM của máy tính bị đầy);

5. Tính độc lập của việc triển khai phần mềm khỏi việc biểu diễn dữ liệu máy và Kiến Trúc Hệ Thống chương trình (lập trình viên tập trung vào chi tiết triển khai chứ không tập trung vào các tính năng biểu diễn dữ liệu máy).

Lưu ý rằng việc nhận ra những lợi ích mà ngôn ngữ lập trình hàm mang lại phụ thuộc rất nhiều vào việc lựa chọn nền tảng phần mềm và phần cứng.

Nếu được chọn là nền tảng phần mềm Công nghệ .NET, hầu như bất kể việc triển khai phần cứng, lập trình viên hoặc người quản lý dự án phần mềm còn nhận được thêm những lợi ích sau:

1. tích hợp các ngôn ngữ lập trình chức năng khác nhau (đồng thời tối đa hóa lợi thế của từng ngôn ngữ, đặc biệt, Đề án cung cấp cơ chế khớp mẫu và SML cung cấp khả năng tính toán khi cần);

2. tích hợp các cách tiếp cận khác nhau để lập trình dựa trên Cơ sở hạ tầng ngôn ngữ chung đa ngôn ngữ hoặc CLI (đặc biệt, có thể sử dụng C# để cung cấp các ưu điểm của cách tiếp cận hướng đối tượng và SML - chức năng, như trong khóa học này) ;

3. Hệ thống gõ thống nhất chung Common Type System, CTS (quản lý thống nhất và an toàn các kiểu dữ liệu trong chương trình);

4. Hệ thống nhiều giai đoạn, linh hoạt đảm bảo tính bảo mật của mã chương trình (đặc biệt dựa trên cơ chế lắp ráp).

Đặc điểm chính của ngôn ngữ lập trình hàm giúp phân biệt chúng với cả ngôn ngữ mệnh lệnh và ngôn ngữ lập trình logic là tính minh bạch tham chiếu và tính tất định. Trong các ngôn ngữ chức năng, có sự khác biệt đáng kể về các tham số như quy tắc gõ và tính toán. Trong nhiều ngôn ngữ, thứ tự đánh giá được xác định chặt chẽ. Nhưng đôi khi các ngôn ngữ nghiêm ngặt có chứa hỗ trợ cho một số thành phần hữu ích vốn có trong các ngôn ngữ không nghiêm ngặt, chẳng hạn như danh sách vô hạn (ML tiêu chuẩn có một mô-đun đặc biệt để hỗ trợ đánh giá lười biếng). Ngược lại, các ngôn ngữ không nghiêm ngặt cho phép tính toán năng lượng trong một số trường hợp.

Do đó, Miranda có ngữ nghĩa lười biếng, nhưng cho phép bạn chỉ định các hàm tạo nghiêm ngặt bằng cách đánh dấu các đối số của hàm tạo theo một cách nhất định.

Nhiều những ngôn ngữ hiện đại ngôn ngữ lập trình chức năng là ngôn ngữ được gõ mạnh (gõ mạnh). Gõ mạnh cung cấp bảo mật cao hơn. Nhiều lỗi có thể được sửa ở giai đoạn biên dịch, do đó giai đoạn gỡ lỗi và thời gian phát triển chương trình tổng thể sẽ giảm xuống. Gõ mạnh cho phép trình biên dịch tạo mã hiệu quả hơn và do đó tăng tốc độ thực thi chương trình. Cùng với đó, còn có các ngôn ngữ chức năng với tính năng gõ động. Kiểu dữ liệu trong các ngôn ngữ đó được xác định trong quá trình thực hiện chương trình (Chương 3). Đôi khi chúng được gọi là "không đánh máy". Ưu điểm của chúng bao gồm thực tế là các chương trình được viết bằng các ngôn ngữ này có tính tổng quát cao hơn. Một nhược điểm có thể được coi là do có nhiều lỗi xảy ra trong giai đoạn thực hiện chương trình và nhu cầu sử dụng các chức năng kiểm tra kiểu liên quan cũng như làm giảm tính tổng quát của chương trình tương ứng. Các ngôn ngữ được gõ góp phần tạo ra mã “đáng tin cậy” hơn, trong khi các ngôn ngữ được gõ tạo ra mã “chung” hơn.

Tiêu chí tiếp theo để phân loại các ngôn ngữ lập trình chức năng có thể là sự hiện diện của các cơ chế mệnh lệnh. Đồng thời, người ta thường gọi các ngôn ngữ lập trình chức năng không có cơ chế mệnh lệnh là “thuần túy” và những ngôn ngữ có chúng được gọi là “không tinh khiết”. Trong tổng quan về các ngôn ngữ lập trình chức năng dưới đây, ngôn ngữ lập trình sẽ được gọi là “thực tế” và “học thuật”. Ngôn ngữ “thực tế” được hiểu là ngôn ngữ có ứng dụng thương mại (các ứng dụng thực tế đã được phát triển trong đó hoặc có hệ thống lập trình thương mại). Ngôn ngữ lập trình học thuật rất phổ biến trong giới nghiên cứu và giáo dục máy tính, nhưng hầu như không có ứng dụng thương mại nào được viết bằng ngôn ngữ đó. Chúng vẫn chỉ là một công cụ để tiến hành nghiên cứu lý thuyết trong lĩnh vực khoa học máy tính và được sử dụng rộng rãi trong quá trình giáo dục.

Dưới đây là danh sách các ngôn ngữ lập trình hàm phổ biến nhất dựa trên các tiêu chí sau: thông tin chung; đánh máy; loại tính toán; sự tinh khiết.

Lisp thông thường. Một phiên bản Lisp có thể coi là chuẩn ngôn ngữ từ năm 1970, nhờ sự hỗ trợ từ Phòng thí nghiệm trí tuệ nhân tạo của Viện Công nghệ Massachusetts, không đánh chữ, tràn đầy năng lượng, với bộ lớn các vùi bắt buộc cho phép phân công và phá hủy các cấu trúc. Thực tế. Chỉ cần nói rằng trình soạn thảo đồ họa vector AutoCAD được viết bằng Lisp.

Cơ chế. Một phương ngữ Lisp dành cho nghiên cứu khoa học về khoa học máy tính và giảng dạy lập trình chức năng. Nhờ thiếu các phần bắt buộc nên ngôn ngữ này nhỏ hơn nhiều so với Common Lisp. Bắt nguồn từ một ngôn ngữ được phát triển bởi J. McCarthy vào năm 1962. Học thuật, không đánh máy, tràn đầy năng lượng, sạch sẽ.

Phản ánh. Một họ ngôn ngữ được phát triển bởi V. F. Turchin. Thành viên lâu đời nhất của gia đình này được thành lập lần đầu tiên vào năm 1968 tại Nga. Nó vẫn được sử dụng rộng rãi trong giới học thuật. Chứa các phần tử lập trình logic (khớp mẫu). Vì vậy, ngôn ngữ Refal được đề xuất trong sách giáo khoa như một ngôn ngữ để tự học.

Miranda. Gõ mạnh, hỗ trợ các kiểu dữ liệu người dùng và đa hình. Được phát triển bởi Turner dựa trên các ngôn ngữ SALS và KRC trước đó. Có ngữ nghĩa lười biếng. Không có sự bao gồm bắt buộc.

Haskell. Sự phát triển của ngôn ngữ xảy ra vào cuối thế kỷ trước. Được biết đến rộng rãi trong giới học thuật. Ở một số trường đại học phương Tây, nó được sử dụng làm ngôn ngữ chính để sinh viên học tập. Một trong những ngôn ngữ chức năng mạnh mẽ nhất. Ngôn ngữ lười biếng. Ngôn ngữ chức năng thuần túy. Đã gõ. Haskell là một công cụ tuyệt vời để học và thử nghiệm các kiểu dữ liệu chức năng phức tạp. Các chương trình viết bằng Haskell có kích thước mã đối tượng đáng kể và tốc độ thực thi thấp.

Lau dọn. Một phương ngữ Haskell phù hợp với nhu cầu lập trình thực tế. Giống như Haskell, nó là một ngôn ngữ thuần chức năng, lười biếng, chứa các lớp kiểu. Nhưng Clean cũng chứa những tính năng thú vị mà Haskell không có. Ví dụ: các tính năng bắt buộc trong Clean dựa trên các loại duy nhất, ý tưởng về tính năng này được mượn từ logic tuyến tính. Clean chứa các cơ chế có thể cải thiện đáng kể hiệu quả của các chương trình. Các cơ chế này ngăn chặn rõ ràng tính toán lười biếng. Việc triển khai Clean là một sản phẩm thương mại nhưng có phiên bản miễn phí cho mục đích nghiên cứu và giáo dục.

ML (Ngôn ngữ Meta). Được phát triển bởi một nhóm lập trình viên do Robert Milier đứng đầu vào giữa những năm 70. ở Edinburgh (Edinburgh Logic cho các hàm tính toán). Ý tưởng của ngôn ngữ là tạo ra một cơ chế xây dựng các bằng chứng hình thức trong một hệ thống logic cho các hàm tính toán được. Năm 1983 ngôn ngữ đã được sửa đổi để bao gồm các khái niệm như mô-đun. Được gọi là ML tiêu chuẩn. ML là một ngôn ngữ được định kiểu mạnh với tính năng kiểm tra kiểu tĩnh và thực thi chương trình ứng dụng. Nó đã trở nên phổ biến rộng rãi trong giới nghiên cứu và trong lĩnh vực giáo dục máy tính.

Đảo ngược chuỗi(String arg) ( if(arg.length == 0) ( return arg; ) else ( return Reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); ) )
Chức năng này khá chậm vì nó gọi chính nó nhiều lần. Có thể có rò rỉ bộ nhớ ở đây vì các đối tượng tạm thời được tạo nhiều lần. Nhưng đây là một phong cách chức năng. Bạn có thể thấy lạ khi mọi người có thể lập trình như thế này. À, tôi vừa định nói với bạn.

Lợi ích của lập trình chức năng

Có lẽ bạn đang nghĩ rằng tôi không thể đưa ra lý do nào để biện minh cho đặc điểm quái dị ở trên. Khi mới bắt đầu học lập trình hàm, tôi cũng nghĩ như vậy. Tôi đã sai. Có những lập luận rất tốt cho phong cách này. Một số trong số đó là chủ quan. Ví dụ, các lập trình viên cho rằng các chương trình chức năng dễ hiểu hơn. Tôi sẽ không đưa ra những lập luận như vậy, bởi ai cũng biết rằng dễ hiểu là một điều rất chủ quan. May mắn cho tôi là vẫn còn rất nhiều lý lẽ khách quan.

Kiểm tra đơn vị

Vì mỗi ký hiệu trong FP là bất biến nên các hàm không có tác dụng phụ. Bạn không thể thay đổi giá trị của biến và một hàm không thể thay đổi giá trị ngoài phạm vi của nó và do đó ảnh hưởng đến các hàm khác (điều có thể xảy ra với các trường lớp hoặc biến toàn cục). Điều này có nghĩa là kết quả duy nhất của việc thực thi hàm là giá trị trả về. Và điều duy nhất có thể ảnh hưởng đến giá trị trả về là các đối số được truyền cho hàm.

Đây rồi, giấc mơ xanh của những người thử nghiệm đơn vị. Bạn có thể kiểm tra mọi chức năng trong một chương trình chỉ bằng cách sử dụng các đối số được yêu cầu. Không cần phải gọi các hàm trong yêu cầu hợp lý hoặc tạo lại trạng thái bên ngoài chính xác. Tất cả những gì bạn cần làm là truyền các đối số khớp với các trường hợp đặc biệt. Nếu tất cả các chức năng trong chương trình của bạn vượt qua các bài kiểm tra đơn vị, thì bạn có thể tự tin hơn nhiều về chất lượng phần mềm của mình so với trường hợp ngôn ngữ lập trình bắt buộc. Trong Java hoặc C++, việc kiểm tra giá trị trả về là chưa đủ - hàm có thể thay đổi trạng thái bên ngoài, điều này cũng phải được kiểm tra. Không có vấn đề như vậy trong FP.

Gỡ lỗi

Nếu một chương trình chức năng không hoạt động như bạn mong đợi thì việc gỡ lỗi chỉ là chuyện dễ dàng. Bạn luôn có thể tái tạo vấn đề vì lỗi trong hàm không phụ thuộc vào mã nước ngoàiđã được thực hiện trước đó. Trong chương trình mệnh lệnh, lỗi chỉ xuất hiện trong một thời gian. Bạn sẽ phải trải qua một số bước không có lỗi do hoạt động của hàm phụ thuộc vào trạng thái bên ngoài và tác dụng phụ của các hàm khác. Trong FP, tình huống đơn giản hơn nhiều - nếu giá trị trả về không chính xác thì nó sẽ luôn không chính xác, bất kể đoạn mã nào được thực thi trước đó.

Khi bạn tái hiện lỗi, hãy tìm nguồn của nó - nhiệm vụ tầm thường. Nó thậm chí còn tốt đẹp. Ngay khi bạn dừng chương trình, bạn sẽ có toàn bộ ngăn xếp cuộc gọi trước mặt. Bạn có thể xem các đối số cho từng lệnh gọi hàm, giống như trong ngôn ngữ mệnh lệnh. Với sự khác biệt là trong một chương trình mệnh lệnh, điều này là không đủ, bởi vì các hàm phụ thuộc vào giá trị của các trường, biến toàn cục và trạng thái của các lớp khác. Một hàm trong FP chỉ phụ thuộc vào các đối số của nó và thông tin này ở ngay trước mắt bạn! Hơn nữa, trong một chương trình mệnh lệnh, việc kiểm tra giá trị trả về là không đủ để biết liệu một đoạn mã có hoạt động chính xác hay không. Bạn sẽ phải truy lùng hàng tá đối tượng bên ngoài hàm để đảm bảo mọi thứ hoạt động chính xác. Trong lập trình hàm, tất cả những gì bạn phải làm là nhìn vào giá trị trả về!

Khi duyệt qua ngăn xếp, bạn chú ý đến các đối số được truyền và giá trị trả về. Khi giá trị trả về lệch khỏi định mức, bạn sẽ đi sâu vào hàm và tiếp tục. Điều này được lặp lại nhiều lần cho đến khi bạn tìm ra nguồn gốc của lỗi!

Đa luồng

Chương trình chức năng ngay lập tức sẵn sàng để song song hóa mà không có bất kỳ thay đổi nào. Bạn không phải lo lắng về tình trạng bế tắc hoặc điều kiện cuộc đua vì bạn không cần khóa! Không một phần dữ liệu nào trong một chương trình chức năng bị thay đổi hai lần bởi cùng một luồng hoặc bởi các luồng khác nhau. Điều này có nghĩa là bạn có thể dễ dàng thêm các luồng vào chương trình của mình mà không phải lo lắng về các vấn đề cố hữu trong các ngôn ngữ mệnh lệnh.

Nếu đúng như vậy thì tại sao các ngôn ngữ lập trình hàm lại hiếm khi được sử dụng trong các ứng dụng đa luồng? Trên thực tế thường xuyên hơn bạn nghĩ. Ericsson đã phát triển một ngôn ngữ chức năng có tên Erlang để sử dụng trên các thiết bị chuyển mạch viễn thông có khả năng chịu lỗi và có thể mở rộng. Nhiều người ghi nhận những ưu điểm của Erlang và bắt đầu sử dụng nó. Chúng ta đang nói về các hệ thống viễn thông và điều khiển giao thông, những hệ thống này gần như không dễ mở rộng quy mô như hệ thống điển hình, được phát triển ở Phố Wall. Trên thực tế, các hệ thống được viết bằng Erlang không có khả năng mở rộng và đáng tin cậy như các hệ thống Java. Hệ thống Erlang đơn giản là siêu đáng tin cậy.

Câu chuyện đa luồng không kết thúc ở đó. Nếu bạn đang viết một ứng dụng đơn luồng, trình biên dịch vẫn có thể tối ưu hóa chương trình chức năng để sử dụng nhiều CPU. Chúng ta hãy xem đoạn mã tiếp theo.


Trình biên dịch ngôn ngữ chức năng có thể phân tích mã, phân loại các hàm tạo ra dòng s1 và s2 là các hàm tốn thời gian và chạy chúng song song. Điều này không thể thực hiện được bằng ngôn ngữ mệnh lệnh, vì mỗi hàm có thể thay đổi trạng thái bên ngoài và mã ngay sau lệnh gọi có thể phụ thuộc vào nó. Trong FP phân tích tự động các chức năng và tìm kiếm các ứng cử viên phù hợp để song song hóa là một nhiệm vụ tầm thường, giống như nội tuyến tự động! Theo nghĩa này, phong cách lập trình chức năng là minh chứng cho tương lai. Các nhà phát triển phần cứng không còn có thể làm cho CPU hoạt động nhanh hơn nữa. Thay vào đó, họ đang tăng số lượng lõi và tuyên bố tốc độ tính toán đa luồng tăng gấp bốn lần. Tất nhiên, họ quên nói đúng lúc rằng bộ xử lý mới của bạn sẽ chỉ đạt được lợi ích trong các chương trình được thiết kế có tính đến tính năng song song. Có rất ít những thứ này trong số các phần mềm bắt buộc. Nhưng 100% các chương trình chức năng đã sẵn sàng cho đa luồng.

Triển khai nóng

Ngày xưa cài đặt Cập nhật Windows Tôi đã phải khởi động lại máy tính. Nhiều lần. Sau khi cài đặt phiên bản mới của trình phát media. Đã có những thay đổi đáng kể trong Windows XP, nhưng tình hình vẫn chưa đạt đến mức lý tưởng (tôi đã chạy Windows Update tại nơi làm việc hôm nay và bây giờ lời nhắc nhở khó chịu sẽ không để tôi yên cho đến khi tôi khởi động lại). Hệ thống Unix có mô hình cập nhật tốt hơn. Để cài đặt các bản cập nhật, tôi phải dừng một số thành phần chứ không phải toàn bộ hệ điều hành. Mặc dù tình hình có vẻ tốt hơn nhưng nó vẫn không được chấp nhận đối với một lượng lớn ứng dụng máy chủ. Hệ thống viễn thông phải được bật 100% mọi lúc, vì nếu một bản cập nhật ngăn cản một người gọi xe cấp cứu thì có thể mất mạng. Các công ty Phố Wall cũng không muốn tắt máy chủ vào cuối tuần để cài đặt các bản cập nhật.

Lý tưởng nhất là bạn cần cập nhật tất cả các phần cần thiết của mã mà không dừng hệ thống về nguyên tắc. Trong thế giới mệnh lệnh, điều này là không thể [transl. trong Smalltalk thì điều đó là rất có thể]. Hãy tưởng tượng bạn đang tải một lớp Java xuống và tải lại một phiên bản mới. Nếu chúng ta làm điều này thì tất cả các phiên bản của lớp sẽ không hoạt động vì trạng thái mà chúng lưu trữ sẽ bị mất. Chúng tôi sẽ phải viết mã phức tạp để kiểm soát phiên bản. Chúng ta sẽ phải tuần tự hóa tất cả các phiên bản đã tạo của lớp, sau đó hủy chúng, tạo các phiên bản của một lớp mới, cố gắng tải dữ liệu được tuần tự hóa với hy vọng quá trình di chuyển sẽ diễn ra suôn sẻ và các phiên bản mới sẽ hợp lệ. Và bên cạnh đó, mã di chuyển phải được viết thủ công mỗi lần. Và mã di chuyển phải bảo toàn các liên kết giữa các đối tượng. Về lý thuyết thì ổn, nhưng trên thực tế thì nó sẽ không bao giờ có tác dụng.

Trong một chương trình chức năng, tất cả trạng thái được lưu trữ trên ngăn xếp dưới dạng đối số của hàm. Điều này làm cho việc triển khai nóng dễ dàng hơn nhiều! Về cơ bản, tất cả những gì bạn cần làm là tính toán sự khác biệt giữa mã trên máy chủ sản xuất và phiên bản mới, đồng thời cài đặt các thay đổi trong mã. Phần còn lại sẽ được thực hiện tự động bởi các công cụ ngôn ngữ! Nếu bạn nghĩ đây là khoa học viễn tưởng, hãy suy nghĩ kỹ. Các kỹ sư làm việc với Erlang đã cập nhật hệ thống của họ trong nhiều năm mà không ngừng công việc.

Bằng chứng và tối ưu hóa được hỗ trợ bằng máy

Một đặc tính thú vị khác của ngôn ngữ lập trình hàm là chúng có thể được học từ quan điểm toán học. Vì ngôn ngữ chức năng là sự triển khai của một hệ thống hình thức nên tất cả các phép toán được sử dụng trên giấy đều có thể được áp dụng cho các chương trình chức năng. Ví dụ, trình biên dịch có thể chuyển đổi một đoạn mã thành một đoạn mã tương đương nhưng hiệu quả hơn, đồng thời chứng minh tính tương đương của chúng về mặt toán học. Cơ sở dữ liệu quan hệ đã thực hiện những tối ưu hóa này trong nhiều năm. Không có gì ngăn cản bạn sử dụng các kỹ thuật tương tự trong các chương trình thông thường.

Ngoài ra, bạn có thể sử dụng toán học để chứng minh tính đúng đắn của các phần trong chương trình của mình. Nếu muốn, bạn có thể viết các công cụ phân tích mã của mình và tự động tạo các bài kiểm tra Đơn vị cho các trường hợp đặc biệt! Chức năng này là vô giá đối với các hệ thống đá rắn. Khi phát triển hệ thống giám sát máy tạo nhịp tim hoặc quản lý không lưu, những công cụ như vậy là bắt buộc. Nếu sự phát triển của bạn không nằm trong khu vực quan trọng ứng dụng quan trọng, thì các công cụ xác minh tự động vẫn sẽ mang lại cho bạn lợi thế to lớn so với đối thủ cạnh tranh.

Hàm bậc cao hơn

Hãy nhớ rằng, khi nói về lợi ích của FP, tôi đã lưu ý rằng “mọi thứ trông có vẻ ổn, nhưng sẽ vô ích nếu tôi phải viết bằng một ngôn ngữ vụng về mà mọi thứ đều là cuối cùng”. Đây là một quan niệm sai lầm. Việc sử dụng Final ở mọi nơi chỉ có vẻ vụng về trong các ngôn ngữ lập trình mệnh lệnh như Java. Các ngôn ngữ lập trình hàm hoạt động với các kiểu trừu tượng khác, những ngôn ngữ khiến bạn quên mất rằng bạn từng thích thay đổi các biến. Một công cụ như vậy là các hàm bậc cao hơn.

Trong FP, một hàm không giống như một hàm trong Java hoặc C. Nó là một siêu tập hợp - chúng có thể thực hiện những điều tương tự như các hàm Java và thậm chí còn hơn thế nữa. Giả sử chúng ta có một hàm trong C:

Int add(int i, int j) ( return i + j; )
Trong FP, hàm này không giống với hàm C thông thường. Hãy mở rộng trình biên dịch Java của chúng tôi để hỗ trợ ký hiệu này. Trình biên dịch phải biến phần khai báo hàm thành mã Java sau (hãy nhớ rằng luôn có một phần cuối ẩn ở mọi nơi):

Lớp add_function_t ( int add(int i, int j) ( return i + j; ) ) add_function_t add = new add_function_t();
Biểu tượng thêm không thực sự là một chức năng. Đây là một lớp học nhỏ với một phương pháp. Bây giờ chúng ta có thể chuyển add làm đối số cho các hàm khác. Chúng ta có thể viết nó thành một biểu tượng khác. Chúng ta có thể tạo các phiên bản của add_function_t trong thời gian chạy và chúng sẽ bị trình thu gom rác hủy nếu không còn cần thiết. Hàm trở thành đối tượng cơ bản, giống như số và chuỗi. Các hàm hoạt động trên các hàm (lấy chúng làm đối số) được gọi là các hàm bậc cao hơn. Đừng để điều này làm bạn sợ hãi. Khái niệm về hàm bậc cao gần giống với khái niệm về các lớp Java hoạt động lẫn nhau (chúng ta có thể chuyển các lớp này sang các lớp khác). Chúng ta có thể gọi chúng là “các lớp bậc cao hơn”, nhưng không ai bận tâm đến điều đó vì Java không có một cộng đồng học thuật nghiêm ngặt đằng sau nó.

Bạn nên sử dụng các hàm bậc cao như thế nào và khi nào? Tôi rất vui vì bạn đã hỏi. Bạn viết chương trình của mình dưới dạng một đoạn mã nguyên khối lớn mà không cần lo lắng về hệ thống phân cấp lớp. Nếu bạn thấy một số đoạn mã được lặp lại trong Những nơi khác nhau, bạn đặt nó vào một chức năng riêng (may mắn là trường học vẫn dạy cách làm việc này). Nếu bạn nhận thấy rằng một số logic trong hàm của bạn sẽ hoạt động khác đi trong một số trường hợp thì bạn tạo một hàm bậc cao hơn. Bối rối? Đây ví dụ thực tế từ công việc của tôi.

Giả sử rằng chúng ta có một đoạn mã Java nhận tin nhắn, chuyển đổi nó theo nhiều cách khác nhau và truyền nó đến một máy chủ khác.

Void handMessage(Message msg) ( // ... msg.setClientCode("ABCD_123"); // ... sendMessage(msg); ) // ... )
Bây giờ hãy tưởng tượng rằng hệ thống đã thay đổi và bây giờ bạn cần phân phối tin nhắn giữa hai máy chủ thay vì một. Mọi thứ vẫn không thay đổi ngoại trừ mã máy khách - máy chủ thứ hai muốn nhận mã này ở định dạng khác. Làm thế nào chúng ta có thể đối phó với tình huống này? Chúng ta có thể kiểm tra xem tin nhắn sẽ đi đâu và tùy vào điều này mà đặt mã đúng khách hàng. Ví dụ như thế này:

Lớp MessageHandler ( void handMessage(Message msg) ( // ... if(msg.getDestination().equals("server1") ( msg.setClientCode("ABCD_123"); ) else ( msg.setClientCode("123_ABC") ; ) // ... sendMessage(msg ) // ... )
Nhưng cách tiếp cận này không có quy mô tốt. Khi các máy chủ mới được thêm vào, tính năng này sẽ phát triển tuyến tính và việc thực hiện các thay đổi sẽ trở thành một cơn ác mộng. Sự vật cách tiếp cận định hướng bao gồm việc tách biệt một siêu lớp chung MessageHandler và phân lớp logic để xác định mã máy khách:

Lớp trừu tượng MessageHandler ( void handMessage(Message msg) ( // ... msg.setClientCode(getClientCode()); // ... sendMessage(msg); ) chuỗi trừu tượng getClientCode(); // ... ) lớp MessageHandlerOne mở rộng MessageHandler ( Chuỗi getClientCode() ( return "ABCD_123"; ) ) lớp MessageHandlerTwo mở rộng MessageHandler ( Chuỗi getClientCode() ( return "123_ABCD"; ) )
Bây giờ đối với mỗi máy chủ, chúng ta có thể tạo một thể hiện của lớp tương ứng. Việc thêm máy chủ mới trở nên thuận tiện hơn. Nhưng có rất nhiều văn bản cho một thay đổi nhỏ như vậy. Tôi đã phải tạo hai loại mới chỉ để thêm hỗ trợ cho các mã máy khách khác nhau! Bây giờ, hãy thực hiện tương tự bằng ngôn ngữ của chúng ta với sự hỗ trợ cho các hàm bậc cao hơn:

Lớp MessageHandler ( void handMessage(Message msg, Function getClientCode) ( // ... Message msg1 = msg.setClientCode(getClientCode()); // ... sendMessage(msg1); ) // ... ) String getClientCodeOne( ) ( return "ABCD_123"; ) Chuỗi getClientCodeTwo() ( return "123_ABCD"; ) MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne);
Chúng tôi không tạo kiểu mới hoặc làm phức tạp hệ thống phân cấp lớp. Chúng ta chỉ đơn giản truyền hàm này dưới dạng tham số. Chúng tôi đã đạt được hiệu quả tương tự như trong phiên bản hướng đối tượng, nhưng có một số lợi thế. Chúng tôi không ràng buộc mình với bất kỳ hệ thống phân cấp lớp nào: chúng tôi có thể chuyển bất kỳ hàm nào khác vào thời gian chạy và thay đổi chúng bất kỳ lúc nào, trong khi vẫn duy trì mức độ mô-đun cao với ít mã hơn. Về cơ bản, trình biên dịch đã tạo ra keo dán hướng đối tượng cho chúng ta! Đồng thời, tất cả các ưu điểm khác của FP đều được bảo tồn. Tất nhiên, sự trừu tượng mà các ngôn ngữ chức năng cung cấp không dừng lại ở đó. Các hàm bậc cao hơn chỉ là sự khởi đầu

cà ri

Hầu hết những người tôi gặp đều đã đọc cuốn sách Các mẫu thiết kế của Gang of Four. Bất kỳ lập trình viên có lòng tự trọng nào cũng sẽ nói rằng cuốn sách không gắn liền với bất kỳ ngôn ngữ lập trình cụ thể nào và các mô hình có thể áp dụng cho việc phát triển phần mềm nói chung. Đây là một tuyên bố cao quý. Nhưng tiếc là nó khác xa sự thật.

Ngôn ngữ chức năng có tính biểu cảm đáng kinh ngạc. Trong ngôn ngữ chức năng, bạn sẽ không cần các mẫu thiết kế vì ngôn ngữ này ở cấp độ cao đến mức bạn có thể dễ dàng bắt đầu lập trình theo các khái niệm loại bỏ tất cả các mẫu lập trình đã biết. Một trong những mẫu này là Adaptor (nó khác với Facade như thế nào? Có vẻ như cần ai đó đóng dấu nhiều trang hơn thực hiện đúng các điều khoản của hợp đồng). Mẫu này hóa ra không cần thiết nếu ngôn ngữ hỗ trợ cà ri.

Mẫu Adaptor thường được áp dụng cho đơn vị trừu tượng "tiêu chuẩn" trong Java - lớp. Trong các ngôn ngữ chức năng, mẫu này được áp dụng cho các hàm. Mẫu lấy một giao diện và biến đổi nó thành một giao diện khác theo các yêu cầu nhất định. Dưới đây là một ví dụ về mẫu Adaptor:

Int pow(int i, int j); int vuông(int i) ( return pow(i, 2); )
Mã này điều chỉnh giao diện của hàm nâng một số lên lũy thừa tùy ý phù hợp với giao diện của hàm bình phương một số. Trong giới học thuật, kỹ thuật đơn giản này được gọi là cà ri (theo tên nhà logic học Haskell Curry, người đã thực hiện một loạt thủ thuật toán học để chính thức hóa tất cả). Vì các hàm được sử dụng ở mọi nơi làm đối số trong FP, nên Currying được sử dụng rất thường xuyên để đưa các hàm đến giao diện cần thiết ở nơi này hay nơi khác. Vì giao diện của một hàm là các đối số của nó nên Currying được sử dụng để giảm số lượng đối số (như trong ví dụ trên).

Công cụ này được tích hợp vào các ngôn ngữ chức năng. Bạn không cần phải tạo một hàm bao bọc bản gốc theo cách thủ công. Một ngôn ngữ chức năng sẽ làm mọi thứ cho bạn. Như thường lệ, hãy mở rộng ngôn ngữ của chúng ta bằng cách thêm cà ri.

Hình vuông = int pow(int i, 2);
Với dòng này, chúng ta sẽ tự động tạo một hàm bình phương với một đối số. Hàm mới sẽ gọi hàm pow, thay thế 2 làm đối số thứ hai. Từ góc độ Java, nó sẽ trông như thế này:

Lớp Square_function_t ( int Square(int i) ( return pow(i, 2); ) ) Square_function_t Square = new Square_function_t();
Như bạn có thể thấy, chúng tôi chỉ viết một trình bao bọc trên hàm ban đầu. Trong FP, cà ri chỉ là một cách đơn giản và thuận tiện để tạo các lớp bao bọc. Bạn tập trung vào nhiệm vụ và trình biên dịch sẽ viết mã cần thiết cho bạn! Việc này rất đơn giản và xảy ra mỗi khi bạn muốn sử dụng mẫu Adaptor (trình bao bọc).

Đánh giá lười biếng

Đánh giá lười biếng (hoặc trì hoãn) là một kỹ thuật thú vị có thể thực hiện được khi bạn hiểu triết lý chức năng. Chúng ta đã thấy đoạn mã sau khi nói về đa luồng:

Chuỗi s1 = phần nàoLongOperation1(); Chuỗi s2 = phần nàoLongOperation2(); Chuỗi s3 = nối(s1, s2);
Trong các ngôn ngữ lập trình mệnh lệnh, thứ tự tính toán không đặt ra bất kỳ câu hỏi nào. Vì mỗi hàm có thể ảnh hưởng hoặc phụ thuộc vào trạng thái bên ngoài nên cần phải duy trì thứ tự lệnh gọi rõ ràng: đầu tiên là phần nàoLongOperation1, sau đó phần nàoLongOperation2 và nối ở cuối. Nhưng không phải mọi thứ đều đơn giản như vậy trong các ngôn ngữ chức năng.

Như chúng ta đã thấy trước đó, phần nàoLongOperation1 và phần nàoLongOperation2 có thể chạy đồng thời vì các hàm được đảm bảo không ảnh hưởng hoặc phụ thuộc vào trạng thái chung. Nhưng nếu chúng ta không muốn thực thi chúng cùng lúc thì có nên gọi chúng theo thứ tự không? Câu trả lời là không. Những phép tính này chỉ nên được chạy nếu bất kỳ hàm nào khác phụ thuộc vào s1 và s2 . Chúng ta thậm chí không cần thực thi chúng cho đến khi chúng ta cần chúng bên trong concatenate . Nếu thay vì ghép nối, chúng ta thay thế một hàm, tùy theo điều kiện, sử dụng một trong hai đối số, thì đối số thứ hai thậm chí có thể không được tính toán! Haskell là một ví dụ về ngôn ngữ đánh giá lười biếng. Haskell không đảm bảo bất kỳ lệnh gọi nào (hoàn toàn không!) vì Haskell thực thi mã khi cần.

Tính toán lười biếng có một số ưu điểm cũng như một số nhược điểm. Trong phần tiếp theo, chúng ta sẽ thảo luận về những ưu điểm và tôi sẽ giải thích cách chấp nhận những nhược điểm.

Tối ưu hóa

Đánh giá lười biếng mang lại tiềm năng to lớn cho việc tối ưu hóa. Một trình biên dịch lười biếng xem xét mã giống như cách một nhà toán học xem xét các biểu thức đại số - nó có thể hoàn tác mọi thứ, hủy bỏ việc thực thi một số phần mã nhất định, thay đổi thứ tự các lệnh gọi để có hiệu quả cao hơn, thậm chí sắp xếp mã theo cách để giảm số lượng lỗi mà vẫn đảm bảo tính toàn vẹn của chương trình. Chính xác là thế này lợi thế lớn khi mô tả một chương trình sử dụng các nguyên hàm hình thức nghiêm ngặt - mã tuân theo các định luật toán học và có thể được nghiên cứu phương pháp toán học.

Tóm tắt cấu trúc điều khiển

Tính toán lười biếng cung cấp mức độ trừu tượng cao đến mức có thể thực hiện được những điều tuyệt vời. Ví dụ, hãy tưởng tượng thực hiện cấu trúc điều khiển sau:

Trừ khi(stock.isEuropean()) ( sendToSEC(stock); )
Chúng tôi muốn hàm sendToSEC chỉ được thực thi nếu cổ phiếu không phải ở Châu Âu. Làm thế nào bạn có thể thực hiện trừ khi ? Nếu không đánh giá lười biếng, chúng ta sẽ cần một hệ thống macro, nhưng trong các ngôn ngữ như Haskell thì điều này là không cần thiết. Chúng ta có thể khai báotrừ khi là một hàm!

Vô hiệu trừ khi(điều kiện boolean, Mã danh sách) ( mã if(!condition); )
Lưu ý rằng mã sẽ không được thực thi nếu condition == true . Trong các ngôn ngữ nghiêm ngặt, hành vi này không thể lặp lại vì các đối số sẽ được đánh giá trước trừ khi được gọi.

Cấu trúc dữ liệu vô hạn

Ngôn ngữ lười biếng cho phép bạn tạo cấu trúc dữ liệu vô hạn, điều này khó tạo hơn nhiều trong các ngôn ngữ nghiêm ngặt. - chỉ là không có trong Python]. Ví dụ, hãy tưởng tượng dãy Fibonacci. Rõ ràng, chúng ta không thể tính toán một danh sách vô hạn trong một thời gian hữu hạn mà vẫn lưu trữ nó trong bộ nhớ. Trong các ngôn ngữ nghiêm ngặt như Java, chúng ta chỉ cần viết một hàm trả về một thành viên tùy ý của chuỗi. Trong các ngôn ngữ như Haskell, chúng ta có thể trừu tượng hóa và chỉ cần khai báo một danh sách vô hạn các số Fibonacci. Vì ngôn ngữ lười biếng nên chỉ những phần cần thiết của danh sách thực sự được sử dụng trong chương trình mới được tính toán. Điều này cho phép bạn trừu tượng hóa từ số lượng lớn các vấn đề và xem xét chúng ở cấp độ cao hơn (ví dụ: bạn có thể sử dụng các hàm để xử lý danh sách trên các chuỗi vô hạn).

sai sót

Tất nhiên, pho mát miễn phí chỉ có trong bẫy chuột. Tính toán lười biếng đi kèm với một số nhược điểm. Đây chủ yếu là những thiếu sót do lười biếng. Trong thực tế, thứ tự tính toán trực tiếp thường rất cần thiết. Lấy ví dụ đoạn mã sau:


Trong ngôn ngữ lười biếng, không ai đảm bảo rằng dòng đầu tiên sẽ được thực thi trước dòng thứ hai! Điều này có nghĩa là chúng ta không thể thực hiện I/O, chúng ta không thể sử dụng các hàm gốc một cách bình thường (xét cho cùng, chúng cần được gọi trong theo một thứ tự nhất định, để tính đến tác dụng phụ của chúng) và không thể tương tác với thế giới bên ngoài! Nếu chúng ta đưa ra một cơ chế ra lệnh thực thi mã, chúng ta sẽ mất đi lợi thế về tính chính xác về mặt toán học của mã (và khi đó chúng ta sẽ mất tất cả lợi ích của lập trình hàm). May mắn thay, tất cả không bị mất. Các nhà toán học đã bắt tay vào làm việc và nghĩ ra một số kỹ thuật để đảm bảo rằng các hướng dẫn được thực hiện theo đúng thứ tự mà không làm mất đi tinh thần chức năng. Chúng tôi đã có được điều tốt nhất của cả hai thế giới! Những kỹ thuật như vậy bao gồm các phần tiếp theo, các đơn nguyên và kiểu gõ duy nhất. Trong bài viết này, chúng tôi sẽ làm việc với các phần tiếp theo và để lại các đơn nguyên và cách gõ rõ ràng cho đến lần tiếp theo. Điều thú vị là phần tiếp theo là một thứ rất hữu ích, nó không chỉ được sử dụng để xác định thứ tự tính toán chặt chẽ. Chúng ta cũng sẽ nói về điều này.

Phần tiếp theo

Sự tiếp tục trong lập trình đóng vai trò tương tự như Mật mã Da Vinci trong lịch sử loài người: một tiết lộ đáng ngạc nhiên về bí ẩn lớn nhất của nhân loại. Chà, có thể không hẳn như vậy, nhưng chắc chắn là họ đã xé bỏ lớp bìa, giống như cách bạn đã học cách lấy gốc của -1 ngày xưa.

Khi xem xét các hàm, chúng ta chỉ biết được một nửa sự thật vì chúng ta giả định rằng một hàm trả về một giá trị cho hàm gọi nó. Theo nghĩa này, sự tiếp tục là sự khái quát hóa các chức năng. Một hàm không nhất thiết phải trả quyền điều khiển về vị trí mà nó được gọi nhưng có thể quay lại bất kỳ vị trí nào trong chương trình. "Tiếp tục" là tham số chúng ta có thể truyền cho hàm để biểu thị điểm trả về. Nghe có vẻ đáng sợ hơn nhiều so với thực tế. Chúng ta hãy xem đoạn mã sau:

Int i = cộng(5, 10); int j = hình vuông(i);
Hàm add trả về số 15, được ghi vào i tại vị trí hàm được gọi. Giá trị của i sau đó được sử dụng khi gọi hình vuông. Lưu ý rằng trình biên dịch lười biếng không thể thay đổi thứ tự tính toán, vì dòng thứ hai phụ thuộc vào kết quả của dòng đầu tiên. Chúng ta có thể viết lại mã này bằng cách sử dụng Kiểu chuyển tiếp tục (CPS), trong đó add trả về một giá trị cho hàm bình phương.

Int j = cộng(5, 10, bình phương);
Trong trường hợp này, add nhận được một đối số bổ sung - một hàm sẽ được gọi sau khi add chạy xong. Trong cả hai ví dụ j sẽ bằng 225.

Đây là kỹ thuật đầu tiên cho phép bạn chỉ định thứ tự thực hiện hai biểu thức. Hãy quay lại ví dụ I/O của chúng ta.

System.out.println("Xin vui lòng nhập tên của bạn: "); System.in.readLine();
Hai dòng này độc lập với nhau và trình biên dịch có thể tự do thay đổi thứ tự của chúng theo ý muốn. Nhưng nếu chúng ta viết lại nó trong CPS, thì chúng ta sẽ thêm phần phụ thuộc cần thiết và trình biên dịch sẽ phải thực hiện các phép tính lần lượt!

System.out.println("Xin vui lòng nhập tên của bạn: ", System.in.readLine);
Trong trường hợp này, println sẽ phải gọi readLine , chuyển kết quả của nó và trả về kết quả của readLine ở cuối. Trong biểu mẫu này, chúng ta có thể chắc chắn rằng các hàm này sẽ lần lượt được gọi và readLine hoàn toàn sẽ được gọi (xét cho cùng, trình biên dịch mong đợi nhận được kết quả của thao tác cuối cùng). Trong trường hợp Java, println trả về void. Nhưng nếu một giá trị trừu tượng nào đó (có thể dùng làm đối số cho readLine) được trả về, điều đó sẽ giải quyết được vấn đề của chúng ta! Tất nhiên, việc xây dựng chuỗi chức năng như vậy sẽ làm giảm đáng kể khả năng đọc mã, nhưng điều này có thể giải quyết được. Chúng ta có thể thêm các tính năng cú pháp vào ngôn ngữ của mình để cho phép chúng ta viết các biểu thức như bình thường và trình biên dịch sẽ tự động xâu chuỗi các phép tính. Giờ đây, chúng ta có thể thực hiện các phép tính theo bất kỳ thứ tự nào mà không làm mất đi các lợi thế của FP (bao gồm khả năng nghiên cứu chương trình bằng các phương pháp toán học)! Nếu điều này gây nhầm lẫn, hãy nhớ rằng các hàm chỉ là các thể hiện của một lớp có một thành viên duy nhất. Viết lại ví dụ của chúng ta sao cho println và readLine là các thể hiện của các lớp, điều này sẽ giúp bạn hiểu rõ hơn.

Nhưng lợi ích của các phần tiếp theo không dừng lại ở đó. Chúng ta có thể viết toàn bộ chương trình bằng CPS, sao cho mỗi hàm được gọi với tham số bổ sung, phần tiếp theo mà kết quả được chuyển vào đó. Về nguyên tắc, bất kỳ chương trình nào cũng có thể được dịch sang CPS nếu bạn coi mỗi chức năng là trương hợp đặc biệt sự tiếp nối. Việc chuyển đổi này có thể được thực hiện tự động (trên thực tế, nhiều trình biên dịch làm như vậy).

Ngay sau khi chúng ta chuyển đổi chương trình sang dạng CPS, rõ ràng là mỗi lệnh đều có phần tiếp theo, một hàm mà kết quả sẽ được truyền vào, trong đó chương trình thường xuyên sẽ là một điểm thách thức. Hãy lấy bất kỳ hướng dẫn nào từ ví dụ trước, ví dụ add(5,10) . Trong một chương trình được viết dưới dạng CPS, rõ ràng phần tiếp theo sẽ là gì - đây là hàm add sẽ gọi khi hoàn thành công việc. Nhưng điều gì sẽ tiếp tục trong trường hợp chương trình không phải CPS? Tất nhiên, chúng tôi có thể chuyển đổi chương trình sang CPS, nhưng điều này có cần thiết không?

Hóa ra điều này là không cần thiết. Hãy xem xét kỹ chuyển đổi CPS của chúng tôi. Nếu bạn bắt đầu viết trình biên dịch cho nó, bạn sẽ thấy rằng phiên bản CPS không cần ngăn xếp! Các hàm không bao giờ trả về bất cứ thứ gì, theo nghĩa truyền thống của từ “return”, chúng chỉ đơn giản gọi một hàm khác, thay thế kết quả của phép tính. Không cần phải đẩy các đối số vào ngăn xếp trước mỗi cuộc gọi và sau đó bật chúng trở lại. Chúng ta có thể chỉ cần lưu trữ các đối số ở một số vị trí bộ nhớ cố định và sử dụng lệnh nhảy thay vì lệnh gọi thông thường. Chúng ta không cần lưu trữ các đối số ban đầu, vì chúng sẽ không bao giờ cần thiết nữa, vì các hàm không trả về bất cứ thứ gì!

Do đó, các chương trình kiểu CPS không cần ngăn xếp mà chứa một đối số bổ sung, dưới dạng hàm, để được gọi. Các chương trình kiểu không phải CPS không có đối số bổ sung mà sử dụng ngăn xếp. Những gì được lưu trữ trên ngăn xếp? Chỉ cần các đối số và một con trỏ tới vị trí bộ nhớ nơi hàm sẽ trả về. Chà, bạn đã đoán được chưa? Ngăn xếp lưu trữ thông tin về sự tiếp tục! Một con trỏ tới điểm trả về trên ngăn xếp giống như hàm được gọi trong các chương trình CPS! Để biết phần tiếp theo của add(5,10) là gì, chỉ cần lấy điểm trả về từ ngăn xếp.

Nó không khó lắm. Phần tiếp theo và con trỏ tới điểm trả về thực sự giống nhau, chỉ phần tiếp theo được chỉ định rõ ràng và do đó nó có thể khác với vị trí mà hàm được gọi. Nếu bạn nhớ rằng phần tiếp theo là một hàm và một hàm trong ngôn ngữ của chúng ta được biên dịch thành một thể hiện của một lớp, thì bạn sẽ hiểu rằng một con trỏ tới điểm trả về trên ngăn xếp và một con trỏ tới phần tiếp theo thực sự giống nhau , bởi vì hàm của chúng ta (như một thể hiện của class ) chỉ là một con trỏ. Điều này có nghĩa là tại bất kỳ thời điểm nào trong chương trình của bạn, bạn có thể yêu cầu tiếp tục hiện tại (về cơ bản là thông tin từ ngăn xếp).

Được rồi, bây giờ chúng ta đã hiểu sự tiếp tục hiện tại là gì. Nó có nghĩa là gì? Nếu chúng ta lấy phần tiếp theo hiện tại và lưu nó ở đâu đó, thì chúng ta sẽ lưu trạng thái hiện tại của chương trình - chúng ta sẽ đóng băng nó. Điều này tương tự như chế độ ngủ đông của hệ điều hành. Đối tượng tiếp tục lưu trữ thông tin cần thiết để tiếp tục thực hiện chương trình từ thời điểm đối tượng tiếp tục được yêu cầu. hệ điều hành thực hiện điều này với các chương trình của bạn mọi lúc khi nó chuyển ngữ cảnh giữa các luồng. Điểm khác biệt duy nhất là mọi thứ đều nằm dưới sự kiểm soát của HĐH. Nếu bạn yêu cầu một đối tượng tiếp tục (trong Lược đồ, điều này được thực hiện bằng cách gọi hàm gọi tiếp theo hiện tại), thì bạn sẽ nhận được một đối tượng có phần tiếp tục hiện tại - ngăn xếp (hoặc trong trường hợp CPS, hàm gọi tiếp theo ). Bạn có thể lưu đối tượng này vào một biến (hoặc thậm chí vào đĩa). Nếu bạn quyết định "khởi động lại" chương trình với phần tiếp theo này, thì trạng thái chương trình của bạn sẽ được "chuyển đổi" sang trạng thái tại thời điểm đối tượng tiếp tục được lấy. Điều này cũng giống như việc chuyển sang một luồng bị treo hoặc đánh thức HĐH sau khi ngủ đông. Ngoại trừ việc bạn có thể làm điều này nhiều lần liên tiếp. Sau khi hệ điều hành thức dậy, thông tin ngủ đông sẽ bị hủy. Nếu điều này không được thực hiện thì có thể khôi phục trạng thái hệ điều hành từ cùng một điểm. Nó gần giống như du hành xuyên thời gian. Với phần tiếp theo, bạn có thể mua được!

Việc tiếp tục sẽ hữu ích trong những tình huống nào? Thông thường nếu bạn đang cố gắng mô phỏng trạng thái trong các hệ thống vốn không có trạng thái. Một cách sử dụng tuyệt vời cho tính năng tiếp tục đã được tìm thấy trong các ứng dụng Web (ví dụ: trong khung công tác Seaside cho ngôn ngữ Smalltalk). ASP.NET của Microsoft nỗ lực hết sức để lưu trạng thái giữa các yêu cầu nhằm giúp cuộc sống của bạn dễ dàng hơn. Nếu C# hỗ trợ các phần tiếp theo, độ phức tạp của ASP.NET có thể giảm đi một nửa bằng cách chỉ cần lưu phần tiếp theo và khôi phục nó trong yêu cầu tiếp theo. Theo quan điểm của một lập trình viên Web, sẽ không có một khoảng dừng nào - chương trình sẽ tiếp tục công việc của nó từ dòng tiếp theo! Tiếp tục là một sự trừu tượng cực kỳ hữu ích để giải quyết một số vấn đề. Với việc ngày càng có nhiều khách hàng truyền thống béo chuyển sang Web, tầm quan trọng của việc tiếp tục sẽ chỉ tăng lên theo thời gian.

Khớp mẫu

Việc khớp mẫu không phải là mới hoặc ý tưởng sáng tạo. Trên thực tế, nó ít liên quan đến lập trình chức năng. Lý do duy nhất nó thường được liên kết với FP là vì hiện nay các ngôn ngữ chức năng có tính năng khớp mẫu, nhưng các ngôn ngữ mệnh lệnh thì không.

Hãy bắt đầu phần giới thiệu của chúng tôi về Khớp mẫu bằng ví dụ sau. Đây là hàm tính số Fibonacci trong Java:

Int fib(int n) ( if(n == 0) return 1; if(n == 1) return 1; return fib(n - 2) + fib(n - 1); )
Và đây là một ví dụ bằng ngôn ngữ giống Java có hỗ trợ Khớp mẫu

Int fib(0) ( return 1; ) int fib(1) ( return 1; ) int fib(int n) ( return fib(n - 2) + fib(n - 1); )
Sự khác biệt là gì? Trình biên dịch thực hiện phân nhánh cho chúng ta.

Hãy nghĩ xem, nó rất quan trọng! Nó thực sự không quan trọng lắm. Nó đã được lưu ý rằng một số lượng lớn các hàm chứa các cấu trúc chuyển đổi phức tạp (điều này một phần đúng với các chương trình chức năng) và người ta quyết định nhấn mạnh điểm này. Định nghĩa hàm được chia thành nhiều biến thể và một mẫu được thiết lập thay cho các đối số của hàm (điều này gợi nhớ đến việc nạp chồng phương thức). Khi một lệnh gọi hàm xảy ra, trình biên dịch sẽ so sánh nhanh các đối số với tất cả các định nghĩa và chọn đối số phù hợp nhất. Thông thường sự lựa chọn rơi vào định nghĩa hàm chuyên biệt nhất. Ví dụ: int fib(int n) có thể được gọi khi n bằng 1, nhưng sẽ không, vì int fib(1) là một định nghĩa chuyên biệt hơn.

Việc khớp mẫu thường trông phức tạp hơn trong ví dụ của chúng tôi. Ví dụ: một hệ thống khớp Mẫu phức tạp cho phép bạn viết đoạn mã sau:

Int f(int n< 10) { ... } int f(int n) { ... }
Khi nào việc khớp mẫu hữu ích? Danh sách những trường hợp như vậy dài đến mức đáng ngạc nhiên! Bất cứ khi nào bạn sử dụng các cấu trúc if lồng nhau phức tạp, việc khớp mẫu có thể thực hiện công việc tốt hơn với ít mã hơn. Nghĩ đến ví dụ tốt với hàm WndProc, được triển khai trong mọi chương trình Win32 (ngay cả khi nó bị ẩn khỏi lập trình viên đằng sau hàng rào trừu tượng cao). Thông thường, việc khớp mẫu thậm chí có thể kiểm tra nội dung của bộ sưu tập. Ví dụ: nếu bạn truyền một mảng cho một hàm thì bạn có thể chọn tất cả các mảng có phần tử đầu tiên bằng 1 và phần tử thứ ba lớn hơn 3.

Một ưu điểm khác của So khớp mẫu là nếu bạn thực hiện thay đổi, bạn sẽ không phải tìm hiểu kỹ một hàm lớn. Tất cả những gì bạn cần làm là thêm (hoặc thay đổi) một số định nghĩa hàm. Như vậy, chúng ta đã loại bỏ được cả một lớp hoa văn từ cuốn sách nổi tiếng Gang of Four. Các điều kiện càng phức tạp và phân nhánh thì việc sử dụng Đối sánh mẫu càng hữu ích. Khi bạn bắt đầu sử dụng chúng, bạn sẽ tự hỏi làm thế nào bạn có thể quản lý được mà không có chúng.

Đóng cửa

Cho đến nay, chúng ta đã thảo luận về các tính năng của FP trong bối cảnh các ngôn ngữ chức năng “thuần túy” - những ngôn ngữ triển khai phép tính lambda và không chứa các tính năng mâu thuẫn với hệ thống Giáo hội chính thức. Tuy nhiên, nhiều tính năng của ngôn ngữ hàm được sử dụng ngoài phép tính lambda. Mặc dù việc triển khai một hệ tiên đề rất thú vị xét từ quan điểm lập trình về mặt biểu thức toán học, nhưng điều này có thể không phải lúc nào cũng có thể áp dụng được trong thực tế. Nhiều ngôn ngữ thích sử dụng các thành phần của ngôn ngữ chức năng mà không tuân theo học thuyết chức năng nghiêm ngặt. Một số ngôn ngữ như vậy (ví dụ Common Lisp) không yêu cầu các biến phải là giá trị cuối cùng - giá trị của chúng có thể được thay đổi. Họ thậm chí không yêu cầu các hàm chỉ phụ thuộc vào đối số của chúng—các hàm được phép truy cập trạng thái ngoài phạm vi của chúng. Nhưng đồng thời chúng bao gồm các tính năng như hàm bậc cao hơn. Việc truyền một hàm bằng ngôn ngữ không thuần túy hơi khác so với thao tác tương tự trong phép tính lambda và cần có sự hiện diện của tính năng thú vịđược gọi là: đóng từ vựng. Chúng ta hãy xem ví dụ sau. Hãy nhớ rằng trong trường hợp này các biến không phải là biến cuối cùng và hàm có thể truy cập các biến nằm ngoài phạm vi của nó:

Hàm makePowerFn(int power) ( int powerFn(int base) ( return pow(base, power); ) trả về powerFn; ) Hàm bình phương = makePowerFn(2); hình vuông(3); // trả về 9
Hàm make-power-fn trả về một hàm nhận một đối số và nâng nó lên một mức độ nhất định. Điều gì xảy ra khi chúng ta cố gắng tính bình phương (3)? Biến power nằm ngoài phạm vi của powerFn vì makePowerFn đã hoàn thành và ngăn xếp của nó đã bị hủy. Vậy hình vuông hoạt động như thế nào? Ngôn ngữ bằng cách nào đó phải lưu trữ ý nghĩa của sức mạnh để hàm bình phương hoạt động. Điều gì sẽ xảy ra nếu chúng ta tạo một hàm lập phương khác để nâng một số lên lũy thừa ba? Ngôn ngữ sẽ phải lưu trữ hai giá trị power cho mỗi hàm được tạo trong make-power-fn. Hiện tượng lưu trữ các giá trị này được gọi là đóng cửa. Việc đóng không chỉ bảo toàn các đối số của hàm trên. Ví dụ: một bao đóng có thể trông như thế này:

Hàm makeIncrementer() ( int n = 0; int tăng() ( return ++n; ) ) Hàm inc1 = makeIncrementer(); Hàm inc2 = makeIncrementer(); inc1(); // trả về 1; inc1(); // trả về 2; inc1(); // trả về 3; inc2(); // trả về 1; inc2(); // trả về 2; inc2(); // trả về 3;
Trong quá trình thực thi, các giá trị của n được lưu trữ và bộ đếm có quyền truy cập vào chúng. Hơn nữa, mỗi bộ đếm có bản sao n riêng, mặc dù thực tế là chúng lẽ ra phải biến mất sau khi hàm makeIncrementer chạy. Trình biên dịch quản lý việc biên dịch này như thế nào? Điều gì xảy ra đằng sau hậu trường đóng cửa? May mắn thay chúng ta có một đường chuyền thần kỳ.

Mọi thứ được thực hiện khá logic. Thoạt nhìn, rõ ràng là các biến cục bộ không còn tuân theo các quy tắc phạm vi nữa và thời gian tồn tại của chúng không được xác định. Rõ ràng, chúng không còn được lưu trữ trên ngăn xếp nữa - chúng cần được giữ trên đống. Do đó, việc đóng được thực hiện giống như hàm bình thường mà chúng ta đã thảo luận trước đó, ngoại trừ việc nó có liên kết bổ sungđến các biến xung quanh:

Lớp some_function_t ( SymbolTable parentScope; // ... )
Nếu một bao đóng truy cập vào một biến không nằm trong phạm vi cục bộ thì nó sẽ tính đến phạm vi cha. Đó là tất cả! Closure kết nối thế giới chức năng với thế giới OOP. Mỗi khi bạn tạo một lớp lưu trữ một số trạng thái và chuyển nó đi đâu đó, hãy nhớ về các bao đóng. Việc đóng chỉ là một đối tượng tạo ra các "thuộc tính" một cách nhanh chóng, đưa chúng ra khỏi phạm vi để bạn không phải tự mình thực hiện.

Giờ thì sao?

Bài viết này chỉ đề cập đến phần nổi của tảng băng chìm Lập trình chức năng. Bạn có thể tìm hiểu sâu hơn và thấy điều gì đó thực sự lớn lao, và trong trường hợp của chúng tôi, điều gì đó tốt đẹp. Trong tương lai, tôi dự định viết về lý thuyết danh mục, đơn nguyên, cấu trúc dữ liệu chức năng, hệ thống kiểu trong ngôn ngữ chức năng, đa luồng chức năng, cơ sở dữ liệu chức năng và nhiều thứ khác. Nếu tôi có thể viết (và nghiên cứu trong quá trình đó) về một nửa số chủ đề này thì cuộc đời tôi sẽ không vô ích. Trong lúc đó, Google- người bạn trung thành của bạn. Tập X được gọi là miền định nghĩa của hàm P và tập Y là miền giá trị của hàm P. Giá trị x trong P(x), đại diện cho bất kỳ phần tử nào từ tập X, được gọi là một biến độc lập và giá trị y từ tập hợp Y, được xác định bởi phương trình y = P(x ), được gọi là biến phụ thuộc. Đôi khi, nếu hàm f không được xác định cho mọi x trong X, thì chúng nói về một hàm được xác định một phần, nếu không thì chúng có nghĩa là độ nét đầy đủ.

Rất tài sản quan trọngĐịnh nghĩa toán học của hàm là, với một đối số x, nó luôn xác định cùng một giá trị vì nó không có tác dụng phụ. Tác dụng phụ trong ngôn ngữ lập trình có liên quan đến các biến mô hình hóa các ô nhớ. hàm toán học xác định một giá trị, thay vì chỉ định một chuỗi các thao tác trên các số được lưu trong các ô bộ nhớ để tính toán một số giá trị. Không có biến nào theo nghĩa được gán cho chúng trong các ngôn ngữ lập trình mệnh lệnh, vì vậy sẽ không có tác dụng phụ.

Trong ngôn ngữ lập trình dựa trên khái niệm toán học về hàm, các chương trình, thủ tục và hàm có thể được biểu diễn. Trong trường hợp chương trình, x tương ứng với dữ liệu đầu vào và y tương ứng với kết quả. Trong trường hợp một thủ tục hoặc hàm, x mô tả các tham số và y mô tả các giá trị trả về. Trong cả hai trường hợp, x có thể được gọi là "đầu vào" và y là "đầu ra". Do đó, trong cách tiếp cận chức năng đối với lập trình, không có sự phân biệt giữa chương trình, thủ tục và chức năng. Mặt khác, số lượng đầu vào và đầu ra luôn khác nhau.

Các ngôn ngữ lập trình có sự phân chia rất rõ ràng giữa việc định nghĩa hàm và sử dụng hàm. Định nghĩa hàm mô tả cách tính một đại lượng dựa trên các tham số hình thức. Ứng dụng hàm là gọi một hàm cụ thể bằng cách sử dụng các tham số thực tế. Lưu ý rằng trong toán học, sự khác biệt giữa một tham số và một biến không phải lúc nào cũng rõ ràng. Rất thường xuyên, thuật ngữ “tham số” được thay thế bằng thuật ngữ “biến độc lập”. Ví dụ, trong toán học, bạn có thể viết định nghĩa của hàm bình phương: bình phương(x) = x * x

Giả sử rằng x thỏa mãn bình phương(x) + 2 . . .

Sự khác biệt chính giữa lập trình mệnh lệnh và lập trình hàm là cách giải thích khái niệm biến. Trong toán học, các biến được biểu diễn dưới dạng giá trị thực và trong các ngôn ngữ lập trình mệnh lệnh, các biến tham chiếu đến các vị trí bộ nhớ nơi lưu trữ giá trị của chúng. Bài tập cho phép bạn thay đổi giá trị trong các vùng bộ nhớ này. Ngược lại, trong toán học không có khái niệm “diện tích lưu trữ” và “phân công” nên một toán tử như x = x + 1

không có ý nghĩa Lập trình hàm dựa trên cách tiếp cận toán học đối với khái niệm "biến". Trong lập trình hàm, các biến được liên kết với các giá trị nhưng không liên quan gì đến vị trí bộ nhớ. Sau khi liên kết một biến với một giá trị, giá trị của biến sẽ thay đổi

Ngôn ngữ lập trình chức năng

Các thuộc tính chính của ngôn ngữ lập trình hàm thường được coi là [ bởi ai?] như sau:

  • ngắn gọn và đơn giản;

Các chương trình trong ngôn ngữ chức năng thường ngắn hơn và đơn giản hơn nhiều so với các chương trình tương tự trong ngôn ngữ mệnh lệnh.
Ví dụ (Hoare quicksort bằng ngôn ngữ chức năng trừu tượng):

QuickSort() =
quickSort() = quickSort(n | n t, n<= h) + [h] + quickSort (n | n t, n >h)

  • gõ mạnh;

Trong các ngôn ngữ chức năng, hầu hết các lỗi có thể được sửa ở giai đoạn biên dịch, do đó giai đoạn gỡ lỗi và thời gian phát triển chương trình tổng thể sẽ giảm xuống. Ngoài ra, gõ mạnh cho phép trình biên dịch tạo mã hiệu quả hơn và do đó tăng tốc độ thực thi chương trình.

  • tính mô-đun;

Cơ chế mô-đun cho phép bạn chia các chương trình thành nhiều phần (mô-đun) tương đối độc lập với các kết nối được xác định rõ ràng giữa chúng. Điều này tạo điều kiện thuận lợi cho việc thiết kế và hỗ trợ tiếp theo cho các hệ thống phần mềm lớn. Hỗ trợ tính mô-đun không phải là đặc tính của các ngôn ngữ lập trình chức năng, nhưng được hầu hết các ngôn ngữ như vậy hỗ trợ.

  • hàm là đối tượng tính toán;

Trong các ngôn ngữ hàm (cũng như trong ngôn ngữ lập trình và toán học nói chung), hàm có thể được truyền cho các hàm khác dưới dạng đối số hoặc kết quả được trả về. Các hàm lấy đối số của hàm được gọi là hàm hoặc hàm bậc cao hơn.

Trong lập trình hàm thuần túy không có toán tử gán, các đối tượng không thể bị sửa đổi hoặc hủy bỏ, các đối tượng mới chỉ có thể được tạo bằng cách phân tách và tổng hợp các đối tượng hiện có. Trình thu gom rác được tích hợp trong ngôn ngữ sẽ xử lý các đối tượng không cần thiết. Nhờ đó, trong các ngôn ngữ chức năng thuần túy, tất cả các chức năng đều không có tác dụng phụ.

  • tính toán trì hoãn (lười biếng).

Trong các ngôn ngữ lập trình truyền thống (chẳng hạn như C++), việc gọi một hàm sẽ khiến tất cả các đối số được đánh giá. Phương pháp gọi hàm này được gọi là gọi theo giá trị. Nếu bất kỳ đối số nào không được sử dụng trong hàm thì kết quả của phép tính sẽ bị mất, do đó, phép tính được thực hiện vô ích. Theo một nghĩa nào đó, đối lập với gọi theo giá trị là gọi theo nhu cầu (đánh giá lười biếng). Trong trường hợp này, đối số chỉ được đánh giá nếu nó cần thiết để tính kết quả.

Một số ngôn ngữ lập trình hàm

  • Gofel
  • MLWorks của Harlequin
  • Phân loại ngôn ngữ chức năng

    Như một ví dụ nguyên chất Một ngôn ngữ chức năng có thể được cung cấp bởi Haskell. Tuy nhiên, hầu hết các ngôn ngữ chức năng đều hỗn hợp và chứa các thuộc tính của cả ngôn ngữ chức năng và mệnh lệnh. Ví dụ sinh động là ngôn ngữ Scala và Nemerle. Chúng kết hợp một cách hữu cơ các đặc điểm của cả ngôn ngữ hướng đối tượng và ngôn ngữ chức năng. Đệ quy đuôi và tối ưu hóa nó được triển khai; hàm này là một đối tượng chính thức, nghĩa là nó có thể được lưu trữ trong một biến, được truyền dưới dạng đối số cho hàm khác hoặc được trả về từ một hàm.

    Ngôn ngữ chức năng cũng được chia thành nghiêm ngặtlỏng lẻo. Các ngôn ngữ không nghiêm ngặt bao gồm những ngôn ngữ hỗ trợ đánh giá lười biếng (F#), nghĩa là các đối số hàm chỉ được đánh giá khi chúng thực sự cần thiết khi đánh giá hàm. Một ví dụ nổi bật về ngôn ngữ không nghiêm ngặt là Haskell. Một ví dụ về ngôn ngữ nghiêm ngặt là ML tiêu chuẩn.

    Một số ngôn ngữ chức năng được triển khai trên các máy ảo hình thành nền tảng (JVM, .NET), nghĩa là các ứng dụng trong các ngôn ngữ này có thể chạy trong môi trường thời gian chạy (JRE, CLR) và sử dụng các lớp tích hợp sẵn. Chúng bao gồm Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

    Liên kết

    • http://fprog.ru/ - Tạp chí “Thực hành lập trình hàm”
    • http://www.intuit.ru/department/pl/funcpl/1/ - Nguyên tắc cơ bản của lập trình hàm. L. V. Gorodnyaya
    • http://roman-dushkin.narod.ru/fp.html - Một khóa học về lập trình hàm, được giảng tại MEPhI từ năm 2001;
    • http://alexott.net/ru/fp/books/ - Đánh giá tài liệu về lập trình hàm. Sách bằng cả tiếng Nga và tiếng Anh đều được xem xét.

    Quỹ Wikimedia. 2010.

    Xem thêm “Ngôn ngữ lập trình hàm” là gì trong các từ điển khác:

      Ngôn ngữ lập trình Lisp- Ngôn ngữ lập trình hàm. chủ đề công nghệ thông tin nói chung EN Lisp... Hướng dẫn dịch thuật kỹ thuật

      Một ngôn ngữ lập trình cấp cao phổ quát. Ngôn ngữ Lisp: dùng để chỉ các ngôn ngữ khai báo thuộc kiểu hàm; được thiết kế để xử lý dữ liệu ký tự được trình bày dưới dạng danh sách. Cơ sở của ngôn ngữ là các hàm và đệ quy... ... Từ điển tài chính

      Thuật ngữ này có ý nghĩa khác, xem Alice. Ngữ nghĩa của Alice: chức năng Kiểu thực thi: biên dịch thành mã byte cho máy ảo Xuất hiện vào: 2002 ... Wikipedia

      Thuật ngữ này có ý nghĩa khác, xem Scala. Lớp ngôn ngữ Scala: Đa mô hình: func... Wikipedia

      Ngữ nghĩa Oz: chức năng, thủ tục, khai báo, hướng đối tượng, tính toán ràng buộc, mô hình H, tính toán song song Loại thực hiện: được biên soạn Xuất hiện vào: 1991 Tác giả: Gert Smolka học trò của ông Phát hành ... Wikipedia

      AWL (Ngôn ngữ web thay thế) Lớp ngôn ngữ: đa mô hình: chức năng, thủ tục, hướng đối tượng Kiểu thực thi: diễn giải Xuất hiện vào: 2005 Kiểu dữ liệu: động ... Wikipedia

      Thuật ngữ này có ý nghĩa khác, xem Leda (ý nghĩa). Leda là ngôn ngữ lập trình đa mô hình được thiết kế bởi Timothy Budd. Ngôn ngữ Leda ban đầu được tạo ra để kết hợp lập trình mệnh lệnh, khách quan... ... Wikipedia

      Erlang Tệp:Erlang logo.png Ngữ nghĩa: đa mô hình: cạnh tranh, lập trình chức năng Xuất hiện vào: 1987 Tác giả: Gõ dữ liệu: nghiêm ngặt, năng động Triển khai chính: E ... Wikipedia

      Trong các ngôn ngữ lập trình hàm, khối xây dựng chính là khái niệm toán học chức năng. Có sự khác biệt trong cách hiểu về hàm trong toán học và hàm trong lập trình, do đó C không thể được phân loại là giống nhau... ... Wikipedia

      Python được hình thành vào những năm 1980 và sự sáng tạo của nó bắt đầu vào tháng 12 năm 1989 bởi Guido van Rossum như một phần của Trung tâm Toán học và Khoa học Máy tính ở Hà Lan. Ngôn ngữ Pythonđược hình thành như hậu duệ của ngôn ngữ lập trình ABC, có khả năng xử lý... ... Wikipedia