Điều gì làm cho các ngôn ngữ lập trình chức năng khai báo trái ngược với mệnh lệnh?


17

Trên nhiều bài viết, mô tả lợi ích của lập trình chức năng, tôi đã thấy các ngôn ngữ lập trình chức năng, như Haskell, ML, Scala hoặc Clojure, được gọi là "ngôn ngữ khai báo" khác với các ngôn ngữ bắt buộc như C / C ++ / C # / Java. Câu hỏi của tôi là những gì làm cho ngôn ngữ lập trình chức năng khai báo trái ngược với mệnh lệnh.

Một lời giải thích thường gặp khi mô tả sự khác biệt giữa lập trình Tuyên bố và Bắt buộc là trong lập trình mệnh lệnh, bạn nói với máy tính "Cách làm một việc gì đó" trái ngược với "Phải làm gì" trong các ngôn ngữ khai báo. Vấn đề tôi có với lời giải thích này là bạn liên tục làm cả hai trong tất cả các ngôn ngữ lập trình. Ngay cả khi bạn đi xuống tổ hợp cấp thấp nhất, bạn vẫn nói với máy tính "Phải làm gì", bạn bảo CPU thêm hai số, bạn không hướng dẫn cách thực hiện phép cộng. Nếu chúng ta đi đến đầu kia của quang phổ, một ngôn ngữ chức năng thuần túy cấp cao như Haskell, thì thực tế bạn đang nói với máy tính cách đạt được một nhiệm vụ cụ thể, đó là những gì chương trình của bạn là một chuỗi các hướng dẫn để đạt được một nhiệm vụ cụ thể mà máy tính không biết làm thế nào để đạt được một mình. Tôi hiểu rằng các ngôn ngữ như Haskell, Clojure, v.v ... rõ ràng là cấp độ cao hơn C / C ++ / C # / Java và cung cấp các tính năng như đánh giá lười biếng, cấu trúc dữ liệu bất biến, chức năng ẩn danh, currying, cấu trúc dữ liệu liên tục, v.v. lập trình chức năng có thể và hiệu quả, nhưng tôi sẽ không phân loại chúng thành ngôn ngữ khai báo.

Một ngôn ngữ khai báo thuần túy đối với tôi sẽ là một ngôn ngữ chỉ bao gồm các khai báo, một ví dụ về các ngôn ngữ như vậy sẽ là CSS (Có tôi biết CSS về mặt kỹ thuật không phải là ngôn ngữ lập trình). CSS chỉ chứa các khai báo kiểu được sử dụng bởi HTML và Javascript của trang. CSS không thể làm gì khác ngoài việc khai báo, nó không thể tạo các hàm lớp, tức là các hàm xác định kiểu hiển thị dựa trên một số tham số, bạn không thể thực thi các tập lệnh CSS, v.v. Điều đó đối với tôi mô tả một ngôn ngữ khai báo (chú ý tôi không nói khai báo ngôn ngữ lập trình ).

Cập nhật:

Gần đây tôi đã chơi với Prolog và với tôi, Prolog là ngôn ngữ lập trình gần nhất với ngôn ngữ khai báo đầy đủ (Ít nhất là theo ý kiến ​​của tôi), nếu đó không phải là ngôn ngữ lập trình khai báo đầy đủ duy nhất. Để xây dựng chương trình trong Prolog được thực hiện bằng cách khai báo trong đó nêu thực tế (một hàm vị ngữ trả về giá trị true cho một đầu vào cụ thể) hoặc một quy tắc (một hàm vị ngữ trả về true cho một điều kiện / mẫu đã cho dựa trên các đầu vào), các quy tắc được xác định bằng cách sử dụng một kỹ thuật khớp mẫu. Để làm bất cứ điều gì trong prolog, bạn truy vấn cơ sở tri thức bằng cách thay thế một hoặc nhiều đầu vào của một vị từ bằng một biến và prolog cố gắng tìm các giá trị cho (các) biến mà vị từ đó thành công.

Quan điểm của tôi là trong prolog không có hướng dẫn bắt buộc, về cơ bản bạn nói (khai báo) máy tính những gì nó biết và sau đó hỏi (truy vấn) về kiến ​​thức. Trong các ngôn ngữ lập trình chức năng, bạn vẫn đưa ra các hướng dẫn, ví dụ như lấy một giá trị, gọi hàm X và thêm 1 vào nó, v.v., ngay cả khi bạn không trực tiếp thao tác các vị trí bộ nhớ hoặc viết ra từng bước tính toán. Tôi sẽ không nói rằng lập trình trong Haskell, ML, Scala hoặc Clojure là khai báo theo nghĩa này, mặc dù tôi có thể sai. Là đúng, đúng, khai báo lập trình chức năng thuần túy theo nghĩa mà tôi đã mô tả ở trên.


@JimmyHoffa ở đâu khi bạn cần anh ấy?

2
1. C # và C ++ hiện đại là những ngôn ngữ rất chức năng. 2. Nghĩ về sự khác biệt của việc viết SQL và Java để đạt được mục đích tương tự (có thể là một số truy vấn phức tạp với các phép nối). bạn cũng có thể nghĩ LINQ trong C # khác với cách viết các vòng lặp foreach đơn giản như thế nào ...
AK_

Ngoài ra, sự khác biệt là vấn đề về phong cách hơn là một thứ gì đó được xác định rõ ràng ... trừ khi bạn đang sử dụng prolog ...
AK_

1
Một trong những chìa khóa để nhận ra sự khác biệt là khi bạn đang sử dụng toán tử gán , so với toán tử khai báo / định nghĩa - ví dụ trong Haskell không có toán tử gán . Hành vi của hai toán tử này rất khác nhau và buộc bạn phải thiết kế mã khác nhau.
Jimmy Hoffa

1
Thật không may, ngay cả khi không có toán tử gán, tôi có thể làm điều này (let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))(Hy vọng bạn hiểu điều đó, Clojure). Câu hỏi ban đầu của tôi là những gì làm cho điều này khác với int này x = 1; x += 2; x *= x; return x;Theo tôi nó gần như giống nhau.
ALXGTV

Câu trả lời:


16

Bạn dường như đang vẽ một đường giữa việc khai báo mọi thứ và hướng dẫn một máy. Không có sự phân tách khó khăn và nhanh chóng như vậy. Bởi vì cỗ máy được hướng dẫn trong lập trình mệnh lệnh không cần phải là phần cứng vật lý, nên có nhiều sự tự do để giải thích. Hầu hết mọi thứ có thể được xem như là một chương trình rõ ràng cho máy trừu tượng đúng. Ví dụ: bạn có thể thấy CSS là ngôn ngữ cấp cao để lập trình một máy chủ yếu giải quyết các bộ chọn và đặt các thuộc tính của các đối tượng DOM được chọn.

Câu hỏi đặt ra là liệu một viễn cảnh như vậy có hợp lý hay không, và ngược lại, trình tự các hướng dẫn gần giống như một tuyên bố về kết quả được tính toán như thế nào. Đối với CSS, quan điểm khai báo rõ ràng hữu ích hơn. Đối với C, quan điểm cấp bách rõ ràng là chiếm ưu thế. Đối với các ngôn ngữ như Haskell, cũng vậy

Ngôn ngữ đã chỉ định, ngữ nghĩa cụ thể. Đó là, tất nhiên người ta có thể hiểu chương trình là một chuỗi các hoạt động. Thậm chí không cần quá nhiều nỗ lực để chọn các hoạt động nguyên thủy để chúng ánh xạ độc đáo vào phần cứng hàng hóa (đây là những gì máy STG và các mô hình khác làm).

Tuy nhiên, cách các chương trình Haskell được viết, chúng thường có thể được đọc một cách hợp lý như là một mô tả về kết quả được tính toán. Lấy ví dụ, chương trình tính tổng của N giai thừa đầu tiên:

sum_of_fac n = sum [product [1..i] | i <- ..n]

Bạn có thể giải mã nó và đọc nó như một chuỗi các hoạt động STG, nhưng tự nhiên hơn nhiều là đọc nó như một mô tả về kết quả (theo tôi, đó là một định nghĩa hữu ích hơn về lập trình khai báo so với điều gì để tính toán): Kết quả là tổng các sản phẩm của [1..i]tất cả i= 0, Mạnh, n. Và đó là khai báo nhiều hơn hầu hết các chương trình hoặc chức năng C.


Lý do tôi hỏi câu hỏi này là vì nhiều người cố gắng thúc đẩy lập trình chức năng vì một số mô hình khác biệt mới tách biệt hoàn toàn với mô hình của C / C ++ / C # / Java hoặc thậm chí Python khi trong lập trình chức năng thực tế chỉ là một hình thức cấp cao hơn khác lập trình mệnh lệnh.
ALXGTV

Để giải thích điều tôi muốn nói ở đây, bạn đang xác định sum_of_fac nhưng sum_of_fac hoàn toàn vô dụng trừ khi đó là những gì ứng dụng lỗ của bạn làm, bạn cần sử dụng kết quả để tính toán một số kết quả khác mà cuối cùng sẽ xây dựng để đạt được một nhiệm vụ cụ thể, nhiệm vụ mà chương trình của bạn được thiết kế để giải quyết.
ALXGTV

6
@ALXGTV Lập trình chức năng một mô hình rất khác nhau, ngay cả khi bạn kiên quyết đọc nó là điều bắt buộc (đó là một phân loại rất rộng, có nhiều mô hình lập trình hơn thế). Và mã khác được xây dựng trên mã này cũng có thể được đọc theo cách khai báo: Ví dụ : map (sum_of_fac . read) (lines fileContent). Tất nhiên tại một số thời điểm I / O được chơi nhưng như tôi đã nói, đó là một sự liên tục. Các phần của chương trình Haskell là bắt buộc hơn, chắc chắn, giống như C có cú pháp biểu thức khai báo sắp xếp ( x + ykhông phải load x; load y; add;)

1
@ALXGTV Trực giác của bạn là chính xác: lập trình khai báo "chỉ" cung cấp mức độ trừu tượng cao hơn các chi tiết bắt buộc nhất định. Tôi cho rằng đây là một vấn đề lớn!
Andres F.

1
@Brendan Tôi không nghĩ lập trình chức năng là một tập hợp con của lập trình mệnh lệnh (cũng không phải là bất biến là một đặc điểm bắt buộc của FP). Có thể lập luận rằng trong trường hợp của FP thuần túy, được gõ tĩnh, nó là một siêu chương trình bắt buộc trong đó các hiệu ứng được thể hiện rõ ràng trong các loại. Bạn biết lời khẳng định miệng lưỡi rằng Haskell là "ngôn ngữ mệnh lệnh tốt nhất thế giới";)
Andres F.

21

Đơn vị cơ bản của một chương trình bắt buộc là tuyên bố . Báo cáo được thực hiện cho các tác dụng phụ của họ. Họ sửa đổi trạng thái họ nhận được. Một chuỗi các câu lệnh là một chuỗi các lệnh, biểu thị làm điều này sau đó làm điều đó. Lập trình viên chỉ định thứ tự chính xác để thực hiện tính toán. Đây là những gì mọi người có nghĩa là bằng cách nói với máy tính làm thế nào để làm điều đó.

Đơn vị cơ bản của một chương trình khai báo là biểu thức . Biểu hiện không có tác dụng phụ. Họ chỉ định mối quan hệ giữa đầu vào và đầu ra, tạo đầu ra mới và riêng biệt với đầu vào của họ, thay vì sửa đổi trạng thái đầu vào của họ. Một chuỗi các biểu thức là vô nghĩa nếu không có một số biểu thức chỉ định mối quan hệ giữa chúng. Lập trình viên chỉ định các mối quan hệ giữa dữ liệu và chương trình đưa ra thứ tự để thực hiện tính toán từ các mối quan hệ đó. Đây là những gì mọi người có nghĩa là bằng cách nói với máy tính những gì để làm.

Các ngôn ngữ bắt buộc có các biểu thức, nhưng phương tiện chính của chúng để hoàn thành công việc là các câu lệnh. Tương tự, các ngôn ngữ khai báo có một số biểu thức có ngữ nghĩa tương tự như các câu lệnh, như các chuỗi đơn nguyên trong doký hiệu của Haskell , nhưng ở cốt lõi của chúng, chúng là một biểu thức lớn. Điều này cho phép người mới bắt đầu viết mã trông rất cấp bách, nhưng sức mạnh thực sự của ngôn ngữ sẽ đến khi bạn thoát khỏi mô hình đó.


1
Đây sẽ là một câu trả lời hoàn hảo nếu nó chứa một ví dụ. Ví dụ tốt nhất mà tôi biết để minh họa declarative vs bắt buộc là LINQ vs foreach.
Robert Harvey

7

Đặc tính xác định thực sự tách biệt khai báo với lập trình mệnh lệnh là theo kiểu khai báo mà bạn không đưa ra các hướng dẫn tuần tự; Ở cấp độ thấp hơn, CPU hoạt động theo cách này, nhưng đó là mối quan tâm của trình biên dịch.

Bạn đề nghị CSS là một "ngôn ngữ khai báo", tôi sẽ không gọi nó là ngôn ngữ nào cả. Đây là định dạng cấu trúc dữ liệu, như JSON, XML, CSV hoặc INI, chỉ là định dạng để xác định dữ liệu mà một trình thông dịch có bản chất biết đến.

Một số tác dụng phụ thú vị xảy ra khi bạn đưa toán tử gán ra khỏi một ngôn ngữ và đây là nguyên nhân thực sự làm mất tất cả các hướng dẫn mệnh lệnh step1-step2-step3 trong các ngôn ngữ khai báo.

Bạn làm gì với toán tử gán trong một hàm? Bạn tạo các bước trung gian. Đó là mấu chốt của nó, toán tử gán được sử dụng để thay đổi dữ liệu theo từng bước. Ngay khi bạn không còn có thể thay đổi dữ liệu, bạn sẽ mất tất cả các bước đó và kết thúc bằng:

Mỗi hàm chỉ có một câu lệnh, câu lệnh này là câu lệnh duy nhất

Bây giờ có nhiều cách để làm cho một tuyên bố giống như nhiều tuyên bố, nhưng đó chỉ là mánh khóe: 1 + 3 + (2*4) + 8 - (7 / (8*3))đây rõ ràng là một tuyên bố duy nhất, nhưng nếu bạn viết nó thành ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

Nó có thể xuất hiện một chuỗi các hoạt động mà não có thể dễ dàng nhận ra sự phân rã mong muốn mà tác giả đang dự định. Tôi thường làm điều này trong C # với mã như ..

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

Đây là một tuyên bố duy nhất, nhưng cho thấy sự xuất hiện của việc bị phân rã thành nhiều - thông báo không có bài tập.

Cũng lưu ý, trong cả hai phương pháp trên - cách mã được thực thi không theo thứ tự bạn đọc ngay lập tức; bởi vì mã này nói phải làm gì, nhưng nó không ra lệnh như thế nào . Lưu ý rằng số học đơn giản ở trên rõ ràng là sẽ không được thực hiện theo trình tự được viết từ trái sang phải và trong ví dụ C # ở trên, ba phương thức này thực sự đều được đăng ký lại và không được thực thi để hoàn thành theo trình tự, trên thực tế trình biên dịch tạo mã điều đó sẽ làm những gì câu nói đó muốn , nhưng không nhất thiết là cách bạn giả định.

Tôi tin rằng việc làm quen với cách tiếp cận không có bước trung gian này của mã hóa khai báo thực sự là phần khó khăn nhất trong tất cả; và tại sao Haskell lại khó khăn như vậy bởi vì rất ít ngôn ngữ thực sự không cho phép nó giống như Haskell. Bạn phải bắt đầu thực hiện một số môn thể dục thú vị để hoàn thành một số điều mà bạn thường làm với sự trợ giúp của các biến trung gian, chẳng hạn như tính tổng.

Trong một ngôn ngữ khai báo, nếu bạn muốn một biến trung gian thực hiện điều gì đó với nó - điều đó có nghĩa là chuyển nó dưới dạng tham số cho hàm thực hiện điều đó. Đây là lý do tại sao đệ quy trở nên rất quan trọng.

sum xs = (head xs) + sum (tail xs)

Bạn không thể tạo một resultSumbiến và thêm vào nó khi bạn lặp qua xs, bạn chỉ cần lấy giá trị đầu tiên và thêm nó vào bất cứ thứ gì là tổng của mọi thứ khác - và để truy cập mọi thứ khác bạn phải truyền xsvào một hàm tailvì bạn không thể tạo một biến cho xs và bật đầu ra, đây sẽ là một cách tiếp cận bắt buộc. (Có, tôi biết bạn có thể sử dụng phá hủy nhưng ví dụ này có ý nghĩa minh họa)


Theo cách hiểu của tôi, từ câu trả lời của bạn, là trong lập trình chức năng thuần túy, toàn bộ chương trình được tạo thành từ nhiều hàm biểu thức đơn, trong khi trong các hàm lập trình (thủ tục) bắt buộc chứa nhiều hơn một biểu thức với một chuỗi hoạt động xác định
ALXGTV

1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) nó thực sự là nhiều câu / biểu thức, tất nhiên nó phụ thuộc vào những gì bạn xem dưới dạng một biểu thức. Tóm lại, lập trình chức năng về cơ bản là lập trình mà không có dấu chấm phẩy.
ALXGTV

1
@ALXGTV sự khác biệt giữa tuyên bố đó và một kiểu mệnh lệnh là nếu biểu thức toán học đó là các câu lệnh, thì bắt buộc chúng phải được thực thi như được viết. Tuy nhiên, vì nó không phải là một chuỗi các câu lệnh bắt buộc mà là một biểu thức xác định một điều bạn muốn, bởi vì nó không nói như thế nào , không bắt buộc nó phải được thực thi như được viết. Do đó, trình biên dịch có thể làm giảm các biểu thức hoặc thậm chí ghi nhớ nó dưới dạng chữ. Các ngôn ngữ bắt buộc cũng làm điều này, nhưng chỉ khi bạn đặt nó trong một câu lệnh; một tuyên bố.
Jimmy Hoffa

Tuyên bố @ALXGTV xác định mối quan hệ, mối quan hệ dễ uốn nắn; nếu x = 1 + 2sau đó x = 3x = 4 - 1. Các câu lệnh tuần tự xác định các hướng dẫn để khi x = (y = 1; z = 2 + y; return z;)bạn không còn có thể điều chỉnh được nữa, trình biên dịch có thể thực hiện theo nhiều cách - thay vào đó, trình biên dịch phải làm theo những gì bạn đã hướng dẫn ở đó, vì trình biên dịch không thể biết tất cả các tác dụng phụ của hướng dẫn của bạn để nó có thể ' t thay đổi chúng.
Jimmy Hoffa

Bạn có một điểm công bằng.
ALXGTV

6

Tôi biết tôi đến bữa tiệc muộn, nhưng tôi đã có một buổi biểu diễn vào một ngày khác nên ở đây nó diễn ra ...

Tôi nghĩ rằng các ý kiến ​​về chức năng, bất biến và không có tác dụng phụ bỏ lỡ dấu hiệu khi giải thích sự khác biệt giữa khai báo so với mệnh lệnh hoặc giải thích ý nghĩa của lập trình khai báo. Ngoài ra, như bạn đã đề cập trong câu hỏi của mình, toàn bộ "phải làm gì" và "làm thế nào" là quá mơ hồ và thực sự cũng không giải thích được.

Hãy lấy mã đơn giản a = b + clàm cơ sở và xem tuyên bố bằng một vài ngôn ngữ khác nhau để có ý tưởng:

Khi chúng ta viết a = b + cbằng một ngôn ngữ bắt buộc, như C, chúng ta sẽ gán giá trị hiện tạib + c cho biến avà không có gì hơn. Chúng tôi không đưa ra bất kỳ tuyên bố cơ bản về những gì alà. Thay vào đó, chúng tôi chỉ đơn giản là thực hiện một bước trong một quy trình.

Khi chúng tôi viết a = b + ctrong một ngôn ngữ khai báo, như Microsoft Excel, (vâng, Excel một ngôn ngữ lập trình và có lẽ là tường thuật hầu hết tất cả,), chúng tôi được khẳng định mối quan hệ giữa a, bcnhư vậy mà nó luôn luôn là trường hợp đó alà tổng của hai cái kia. Đó không phải là một bước trong một quá trình, nó là một bất biến, một sự bảo đảm, một sự tuyên bố về sự thật.

Ngôn ngữ chức năng là khai báo là tốt, nhưng gần như tình cờ. Trong Haskel chẳng hạn a = b + ccũng khẳng định một mối quan hệ bất biến, nhưng chỉ vì bclà bất biến.

Vì vậy, có, khi các đối tượng là bất biến và các hàm không có tác dụng phụ, mã sẽ trở thành khai báo (ngay cả khi nó trông giống với mã mệnh lệnh), nhưng chúng không phải là điểm chính. Cũng không tránh được sự phân công. Điểm của mã khai báo là đưa ra các tuyên bố cơ bản về các mối quan hệ.


0

Bạn đúng rằng không có sự phân biệt rõ ràng giữa việc nói cho máy tính biết phải làm gì và làm như thế nào .

Tuy nhiên, ở một mặt của quang phổ, bạn hầu như chỉ nghĩ về cách thao tác bộ nhớ. Đó là, để giải quyết vấn đề, bạn trình bày nó cho máy tính ở dạng như "đặt vị trí bộ nhớ này thành x, sau đó đặt vị trí bộ nhớ đó thành y, nhảy đến vị trí bộ nhớ z ..." và sau đó bằng cách nào đó bạn sẽ có kết quả ở một số vị trí bộ nhớ khác.

Trong các ngôn ngữ được quản lý như Java, C #, v.v., bạn không có quyền truy cập trực tiếp vào bộ nhớ phần cứng nữa. Lập trình viên bắt buộc hiện có liên quan đến các biến tĩnh, tham chiếu hoặc các trường của các thể hiện của lớp, tất cả đều bị thiếu ở một mức độ nào đó cho các vị trí bộ nhớ.

Trong langugaes như Haskell, OTOH, bộ nhớ hoàn toàn biến mất. Nó chỉ đơn giản là không phải là trường hợp trong

f a b = let y = sum [a..b] in y*y

phải có hai ô nhớ giữ các đối số a và b và một ô khác chứa kết quả trung gian y. Để chắc chắn, một phụ trợ trình biên dịch có thể phát ra mã cuối cùng hoạt động theo cách này (và trong một số trường hợp, nó phải làm như vậy miễn là kiến ​​trúc đích là một máy v. Neumann).

Nhưng vấn đề là chúng ta không cần phải nội hóa kiến ​​trúc v. Neumann để hiểu chức năng trên. Chúng ta cũng không cần một máy tính hiện đại để chạy nó. Chẳng hạn, có thể dễ dàng dịch các chương trình bằng ngôn ngữ FP thuần túy sang một máy giả định hoạt động trên cơ sở tính toán SKI. Bây giờ hãy thử tương tự với một chương trình C!

Một ngôn ngữ khai báo thuần túy đối với tôi sẽ là một ngôn ngữ chỉ bao gồm các khai báo

Điều này không đủ mạnh, IMHO. Ngay cả một chương trình C chỉ là một chuỗi các khai báo. Tôi cảm thấy chúng ta phải đủ điều kiện khai báo hơn nữa. Ví dụ, họ đang nói với chúng ta điều gì (khai báo) hoặc những gì nó làm (bắt buộc).


Tôi không nói rằng lập trình chức năng không khác với lập trình trong C, thực tế tôi đã nói rằng các ngôn ngữ như Haskell ở mức cao hơn NHIỀU so với C và mang lại sự trừu tượng lớn hơn nhiều.
ALXGTV
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.