Thực sự hiểu sự khác biệt giữa thủ tục và chức năng


114

Tôi thực sự gặp khó khăn khi hiểu sự khác biệt giữa các mô hình lập trình thủ tụcchức năng .

Đây là hai đoạn đầu tiên từ mục nhập Wikipedia về lập trình hàm :

Trong khoa học máy tính, lập trình hàm là một mô hình lập trình coi việc tính toán là việc đánh giá các hàm toán học và tránh trạng thái và dữ liệu có thể thay đổi. Nó nhấn mạnh vào việc áp dụng các chức năng, trái ngược với phong cách lập trình mệnh lệnh, nhấn mạnh những thay đổi về trạng thái. Lập trình hàm có nguồn gốc từ giải tích lambda, một hệ thống chính thức được phát triển vào những năm 1930 để nghiên cứu định nghĩa hàm, ứng dụng hàm và đệ quy. Nhiều ngôn ngữ lập trình chức năng có thể được xem như là sự phát triển của giải tích lambda.

Trong thực tế, sự khác biệt giữa hàm toán học và khái niệm "hàm" được sử dụng trong lập trình mệnh lệnh là các hàm mệnh lệnh có thể có tác dụng phụ, làm thay đổi giá trị của trạng thái chương trình. Do đó, chúng thiếu tính minh bạch trong tham chiếu, tức là cùng một biểu thức ngôn ngữ có thể dẫn đến các giá trị khác nhau tại các thời điểm khác nhau tùy thuộc vào trạng thái của chương trình đang thực thi. Ngược lại, trong mã hàm, giá trị đầu ra của một hàm chỉ phụ thuộc vào các đối số được nhập vào hàm, do đó, việc gọi một hàm fhai lần với cùng một giá trị cho một đối số xsẽ tạo ra cùng một kết quảf(x)cả hai lần. Việc loại bỏ các hiệu ứng phụ có thể làm cho việc hiểu và dự đoán hành vi của một chương trình trở nên dễ dàng hơn nhiều, đây là một trong những động lực chính cho sự phát triển của lập trình chức năng.

Trong đoạn 2 nơi nó nói

Ngược lại, trong mã chức năng, giá trị đầu ra của một hàm chỉ phụ thuộc vào các đối số được nhập vào hàm, do đó, việc gọi một hàm fhai lần với cùng một giá trị cho một đối số xsẽ tạo ra cùng một kết quả f(x)cả hai lần.

Đó không phải là trường hợp chính xác tương tự cho lập trình thủ tục?

Điều gì nên tìm kiếm trong thủ tục và chức năng nổi bật?


1
Liên kết "Charming Python: Chức năng lập trình trong Python" từ Abafei đã bị hỏng. Đây là một tập hợp các liên kết tốt: ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Chris Koknat

Một khía cạnh khác của điều này là đặt tên. Ví dụ. trong JavaScript và Common Lisp, chúng tôi sử dụng thuật ngữ hàm mặc dù chúng được cho phép có tác dụng phụ và trong Đề án, hàm này luôn được gọi là thủ tục. Một hàm CL thuần túy có thể được viết dưới dạng một thủ tục Sơ đồ chức năng thuần túy. Hầu hết tất cả các sách về Scheme đều sử dụng thuật ngữ thủ tục vì nó là tyerm được sử dụng trong tiêu chuẩn và nó không liên quan gì đến việc nó là thủ tục hay chức năng.
Sylwester

Câu trả lời:


276

Lập trình chức năng

Lập trình hàm đề cập đến khả năng coi các hàm như các giá trị.

Hãy xem xét một phép loại suy với các giá trị "thông thường". Chúng ta có thể lấy hai giá trị số nguyên và kết hợp chúng bằng +toán tử để thu được một số nguyên mới. Hoặc chúng ta có thể nhân một số nguyên với một số dấu phẩy động để được một số dấu phẩy động.

Trong lập trình hàm, chúng ta có thể kết hợp hai giá trị hàm để tạo ra một giá trị hàm mới bằng cách sử dụng các toán tử như soạn hoặc nâng . Hoặc chúng ta có thể kết hợp một giá trị hàm và một giá trị dữ liệu để tạo ra một giá trị dữ liệu mới bằng cách sử dụng các toán tử như map hoặc fold .

Lưu ý rằng nhiều ngôn ngữ có khả năng lập trình chức năng - ngay cả những ngôn ngữ thường không được coi là ngôn ngữ chức năng. Ngay cả Grandfather FORTRAN cũng hỗ trợ các giá trị hàm, mặc dù nó không cung cấp nhiều trong cách các toán tử kết hợp hàm. Đối với một ngôn ngữ được gọi là "chức năng", nó cần có các khả năng lập trình chức năng theo một cách lớn.

Lập trình thủ tục

Lập trình thủ tục đề cập đến khả năng đóng gói một chuỗi hướng dẫn chung thành một thủ tục để những hướng dẫn đó có thể được gọi từ nhiều nơi mà không cần dùng đến sao chép và dán. Vì các thủ tục đã được phát triển rất sớm trong lập trình, nên khả năng này hầu như luôn được liên kết với phong cách lập trình theo yêu cầu của lập trình máy hoặc ngôn ngữ hợp ngữ: một phong cách nhấn mạnh khái niệm về vị trí lưu trữ và hướng dẫn di chuyển dữ liệu giữa các vị trí đó.

Tương phản

Hai phong cách không thực sự đối lập - chúng chỉ khác nhau. Có những ngôn ngữ bao gồm đầy đủ cả hai kiểu (ví dụ như LISP). Tình huống sau đây có thể cho ta cảm giác về một số khác biệt trong hai phong cách. Hãy viết một số mã cho một yêu cầu vô nghĩa mà chúng ta muốn xác định xem tất cả các từ trong danh sách có một số ký tự lẻ hay không. Đầu tiên, phong cách thủ tục:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

Tôi sẽ coi nó như là một ví dụ này có thể hiểu được. Bây giờ, phong cách chức năng:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Làm việc từ trong ra ngoài, định nghĩa này thực hiện những điều sau:

  1. compose(odd, length)kết hợp các hàm oddlengthđể tạo ra một hàm mới xác định xem độ dài của một chuỗi có phải là số lẻ hay không.
  2. map(..., words)gọi hàm mới đó cho từng phần tử trong words, cuối cùng trả về một danh sách mới các giá trị boolean, mỗi giá trị cho biết từ tương ứng có số ký tự lẻ hay không.
  3. apply(and, ...)áp dụng toán tử "và" cho danh sách kết quả -ing tất cả các boolean cùng nhau để mang lại kết quả cuối cùng.

Bạn có thể thấy từ những ví dụ này rằng lập trình thủ tục rất quan tâm đến việc di chuyển các giá trị xung quanh trong các biến và mô tả rõ ràng các hoạt động cần thiết để tạo ra kết quả cuối cùng. Ngược lại, phong cách chức năng nhấn mạnh sự kết hợp của các chức năng cần thiết để biến đổi đầu vào ban đầu thành đầu ra cuối cùng.

Ví dụ cũng cho thấy các kích thước tương đối điển hình của mã thủ tục so với mã chức năng. Hơn nữa, nó chứng tỏ rằng các đặc tính hiệu suất của mã thủ tục có thể dễ thấy hơn so với mã chức năng. Hãy xem xét: các hàm tính toán độ dài của tất cả các từ trong danh sách, hay mỗi từ dừng lại ngay sau khi tìm thấy độ dài chẵn đầu tiên? Mặt khác, mã chức năng cho phép triển khai chất lượng cao để thực hiện một số tối ưu hóa khá nghiêm trọng vì nó chủ yếu thể hiện ý định hơn là một thuật toán rõ ràng.

Đọc thêm

Câu hỏi này xuất hiện rất nhiều ... hãy xem, ví dụ:

Bài giảng về giải thưởng Turing của John Backus giải thích rất chi tiết các động cơ thúc đẩy lập trình hàm:

Lập trình có thể được giải phóng khỏi phong cách von Neumann không?

Tôi thực sự không nên đề cập đến bài báo đó trong bối cảnh hiện tại vì nó trở nên khá kỹ thuật, khá nhanh. Tôi chỉ không thể cưỡng lại vì tôi nghĩ nó thực sự là cơ sở.


Phụ lục - 2013

Các nhà bình luận chỉ ra rằng các ngôn ngữ hiện đại phổ biến cung cấp các kiểu lập trình khác hơn là thủ tục và chức năng. Các ngôn ngữ như vậy thường cung cấp một hoặc nhiều kiểu lập trình sau:

  • truy vấn (ví dụ: hiểu danh sách, truy vấn tích hợp ngôn ngữ)
  • luồng dữ liệu (ví dụ: lặp lại ngầm định, hoạt động hàng loạt)
  • hướng đối tượng (ví dụ: dữ liệu và phương thức được đóng gói)
  • hướng ngôn ngữ (ví dụ: cú pháp dành riêng cho ứng dụng, macro)

Xem các nhận xét bên dưới để biết ví dụ về cách các ví dụ mã giả trong phản hồi này có thể được hưởng lợi từ một số tiện ích có sẵn từ các kiểu khác đó. Đặc biệt, ví dụ về thủ tục sẽ được hưởng lợi từ việc áp dụng hầu như bất kỳ cấu trúc cấp cao hơn nào.

Các ví dụ được trưng bày cố ý tránh trộn lẫn các phong cách lập trình khác này để nhấn mạnh sự khác biệt giữa hai phong cách đang thảo luận.


1
Quả thực là câu trả lời hay, nhưng bạn có thể đơn giản hóa mã một chút không, ví dụ: "function allOdd (words) {foreach (auto word in words) {lẻ (length (word)? Return false:;} return true;}"
Dainius

Kiểu chức năng ở đó khá khó đọc so với "kiểu chức năng" trong python: def retail_words (words): return [x cho x trong từ nếu lẻ (len (x))]
đóng hộp

@boxed: odd_words(words)Định nghĩa của bạn khác với câu trả lời allOdd. Đối với việc lọc và ánh xạ, việc hiểu danh sách thường được ưu tiên hơn, nhưng ở đây, hàm allOddđược cho là giảm danh sách các từ thành một giá trị boolean duy nhất.
ShinNoNoir

@WReach: Tôi đã viết ví dụ về chức năng của bạn như thế này: function allOdd (words) {return and (retail (length (first (words)))), allOdd (rest (words))); } Nó không thanh lịch hơn ví dụ của bạn, nhưng trong ngôn ngữ đệ quy đuôi, nó sẽ có các đặc điểm hiệu suất giống như kiểu mệnh lệnh.
mishoo

@mishoo Ngôn ngữ cần phải vừa đệ quy đuôi, vừa chặt chẽ và ngắn mạch trong toán tử and để giả định của bạn có thể giữ được, tôi tin.
kqr

46

Sự khác biệt thực sự giữa lập trình hàm và lập trình mệnh lệnh là tư duy - các lập trình viên bắt buộc đang nghĩ đến các biến và khối bộ nhớ, trong khi các lập trình viên hàm đang nghĩ, "Làm thế nào tôi có thể chuyển đổi dữ liệu đầu vào thành dữ liệu đầu ra của mình" - "chương trình" của bạn là đường dẫn và tập hợp các phép biến đổi trên dữ liệu để đưa dữ liệu đó từ Đầu vào đến Đầu ra. Đó là phần thú vị IMO, không phải bit "Bạn sẽ không sử dụng các biến".

Như một hệ quả của tư duy này, các chương trình FP thường mô tả những gì sẽ xảy ra, thay vì cơ chế cụ thể về cách nó sẽ xảy ra - điều này rất mạnh mẽ bởi vì nếu chúng ta có thể nêu rõ ràng "Chọn", "Ở đâu" và "Tổng hợp" nghĩa là gì, chúng ta có thể tự do hoán đổi các triển khai của chúng, giống như chúng tôi làm với AsParallel () và đột nhiên ứng dụng đơn luồng của chúng tôi mở rộng ra n lõi.


bất kỳ cách nào bạn có thể đối chiếu hai bằng cách sử dụng các đoạn mã ví dụ? thực sự đánh giá cao nó
Philoxopher

1
@KerxPhilo: Đây là một công việc rất đơn giản (thêm các số từ 1 đến n). Mệnh lệnh: Sửa đổi số hiện tại, sửa đổi tổng cho đến nay. Mã: int i, sum; tổng = 0; for (i = 1; i <= n; i ++) {sum + = i; }. Chức năng (Haskell): Lấy một danh sách lười biếng các số, gấp chúng lại với nhau trong khi cộng với số không. Mã: foldl (+) 0 [1..n]. Xin lỗi, không có định dạng trong nhận xét.
dirkt

+1 câu trả lời. Nói cách khác, lập trình hàm là viết các hàm mà không có tác dụng phụ bất cứ khi nào có thể, tức là hàm luôn trả về cùng một thứ khi cho cùng các tham số - đó là nền tảng. Nếu bạn làm theo cách tiếp cận đó đến mức cực đoan, các tác dụng phụ của bạn (bạn luôn cần chúng) sẽ bị cô lập và phần còn lại của các chức năng chỉ đơn giản là chuyển đổi dữ liệu đầu vào thành dữ liệu đầu ra.
beluchin

12
     Isn't that the same exact case for procedural programming?

Không, vì mã thủ tục có thể có tác dụng phụ. Ví dụ, nó có thể lưu trữ trạng thái giữa các cuộc gọi.

Điều đó nói rằng, có thể viết mã thỏa mãn ràng buộc này trong các ngôn ngữ được coi là thủ tục. Và cũng có thể viết mã phá vỡ ràng buộc này trong một số ngôn ngữ được coi là chức năng.


1
Bạn có thể chỉ ra một ví dụ và so sánh? Thực sự đánh giá cao nó nếu bạn có thể.
Philoxopher

8
Hàm rand () trong C cung cấp một kết quả khác nhau cho mọi cuộc gọi. Nó lưu trữ trạng thái giữa các cuộc gọi. Nó không minh bạch về mặt quy chiếu. Để so sánh, std :: max (a, b) trong C ++ sẽ luôn trả về cùng một kết quả với các đối số giống nhau và không có tác dụng phụ (mà tôi biết là ...).
Andy Thomas

11

Tôi không đồng ý với câu trả lời của WReach. Hãy giải mã câu trả lời của anh ấy một chút để xem sự bất đồng đến từ đâu.

Đầu tiên, mã của anh ấy:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Điều đầu tiên cần lưu ý là anh ta đang nói:

  • Chức năng
  • Định hướng biểu thức và
  • Trung tâm lặp lại

lập trình và thiếu khả năng lập trình kiểu lặp để có luồng điều khiển rõ ràng hơn kiểu chức năng thông thường.

Hãy nhanh chóng nói về những điều này.

Phong cách tập trung vào biểu hiện là phong cách mà mọi thứ, càng nhiều càng tốt, đánh giá mọi thứ. Mặc dù các ngôn ngữ chức năng nổi tiếng vì tình yêu của chúng với các biểu thức, nhưng thực sự có thể có một ngôn ngữ chức năng mà không có các biểu thức có thể ghép được. Tôi sẽ tạo ra một điều, nơi không có biểu thức, chỉ đơn thuần là các câu lệnh.

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

Điều này khá giống với những gì đã đưa ra trước đây, ngoại trừ các hàm được xâu chuỗi hoàn toàn thông qua các chuỗi câu lệnh và ràng buộc.

Một phong cách lập trình trung tâm của trình vòng lặp có thể được Python sử dụng. Hãy sử dụng một kiểu lặp lại thuần túy , tập trung vào trình lặp:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

Điều này không phải là chức năng, bởi vì mỗi mệnh đề là một quá trình lặp lại và chúng được ràng buộc với nhau bằng cách tạm dừng rõ ràng và tiếp tục các khung ngăn xếp. Cú pháp có thể được lấy cảm hứng một phần từ ngôn ngữ chức năng, nhưng nó được áp dụng cho một phương án lặp lại hoàn toàn của nó.

Tất nhiên, bạn có thể nén:

def all_odd(words):
    return all(odd(len(word)) for word in words)

Bây giờ trông mệnh lệnh không tệ lắm phải không? :)

Điểm cuối cùng là về luồng kiểm soát rõ ràng hơn. Hãy viết lại mã gốc để sử dụng:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

Sử dụng trình vòng lặp, bạn có thể có:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

Vậy điều gì quan điểm của một ngôn ngữ chức năng nếu sự khác biệt là giữa:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


Đặc điểm chính xác nhất của một ngôn ngữ lập trình hàm là nó loại bỏ đột biến như một phần của mô hình lập trình điển hình. Mọi người thường hiểu điều này có nghĩa là một ngôn ngữ lập trình hàm không có câu lệnh hoặc sử dụng biểu thức, nhưng đây là những đơn giản hóa. Một ngôn ngữ chức năng thay thế tính toán rõ ràng bằng một tuyên bố về hành vi, ngôn ngữ này sau đó thực hiện giảm bớt.

Hạn chế bản thân trong tập hợp con chức năng này cho phép bạn có nhiều đảm bảo hơn về các hành vi của chương trình và điều này cho phép bạn soạn chúng một cách tự do hơn.

Khi bạn có một ngôn ngữ hàm, việc tạo các hàm mới thường đơn giản như việc soạn các hàm có liên quan chặt chẽ.

all = partial(apply, and)

Điều này không đơn giản, hoặc thậm chí có thể không thực hiện được nếu bạn chưa kiểm soát rõ ràng các phụ thuộc toàn cục của một hàm. Tính năng tốt nhất của lập trình chức năng là bạn có thể luôn tạo ra nhiều nội dung trừu tượng chung hơn và tin tưởng rằng chúng có thể được kết hợp thành một tổng thể lớn hơn.


Bạn biết đấy, tôi khá chắc chắn rằng một applyhoạt động không hoàn toàn giống với foldhoặc reduce, mặc dù tôi đồng ý với khả năng tuyệt vời là có các thuật toán rất chung chung.
Benedict Lee

Tôi chưa bao giờ nghe nói về applynghĩa là foldhoặc reduce, nhưng với tôi thì có vẻ như nó phải ở trong ngữ cảnh này thì nó mới trả về một boolean.
Veedrac

À, được rồi, tôi thấy bối rối khi đặt tên. Cảm ơn vì đã xóa nó.
Benedict Lee

6

Trong mô hình thủ tục (thay vào đó tôi sẽ nói "lập trình có cấu trúc" chứ?), Bạn đã chia sẻ bộ nhớ có thể thay đổi và hướng dẫn đọc / ghi nó theo một số trình tự (lần lượt).

Trong mô hình hàm, bạn có các biến và hàm (theo nghĩa toán học: các biến không thay đổi theo thời gian, các hàm chỉ có thể tính toán một cái gì đó dựa trên đầu vào của chúng).

(Điều này được đơn giản hóa quá mức, ví dụ, FPL thường có các phương tiện để làm việc với bộ nhớ có thể thay đổi trong khi các ngôn ngữ thủ tục thường có thể hỗ trợ các thủ tục bậc cao hơn nên mọi thứ không rõ ràng như vậy; nhưng điều này sẽ cung cấp cho bạn ý tưởng)


2

Các Charming Python: lập trình chức năng bằng Python từ IBM developerWorks thực sự giúp tôi hiểu sự khác biệt.

Đặc biệt là đối với những người biết Python một chút, các ví dụ mã trong bài viết này tương phản với nhau trong đó làm những việc khác nhau về mặt chức năng và thủ tục, có thể làm rõ sự khác biệt giữa lập trình thủ tục và chức năng.


2

Trong lập trình hàm để suy luận về ý nghĩa của một biểu tượng (tên biến hoặc hàm), bạn thực sự chỉ cần biết 2 điều - phạm vi hiện tại và tên của biểu tượng. Nếu bạn có một ngôn ngữ chức năng thuần túy với tính bất biến, cả hai đều là khái niệm "tĩnh" (xin lỗi vì tên bị quá tải nặng), có nghĩa là bạn có thể thấy cả hai - phạm vi hiện tại và tên - chỉ bằng cách nhìn vào mã nguồn.

Trong lập trình thủ tục, nếu bạn muốn trả lời câu hỏi giá trị đằng sau là xgì, bạn cũng cần biết làm thế nào bạn đến đó, phạm vi và tên gọi thôi là không đủ. Và đây là điều tôi coi là thách thức lớn nhất bởi vì đường dẫn thực thi này là thuộc tính "thời gian chạy" và có thể phụ thuộc vào rất nhiều thứ khác nhau, đến nỗi hầu hết mọi người học cách chỉ gỡ lỗi nó chứ không phải thử và khôi phục đường dẫn thực thi.


1

Gần đây tôi đã nghĩ đến sự khác biệt về vấn đề Biểu thức . Mô tả của Phil Wadler không được trích dẫn, nhưng câu trả lời được chấp nhận cho câu hỏi này có lẽ dễ theo dõi hơn. Về cơ bản, có vẻ như các ngôn ngữ mệnh lệnh có xu hướng chọn một cách tiếp cận vấn đề, trong khi các ngôn ngữ chức năng có xu hướng chọn cách khác.


0

Một sự khác biệt rõ ràng giữa hai mô hình lập trình là trạng thái.

Trong Lập trình Chức năng, trạng thái được tránh. Nói một cách đơn giản, sẽ không có biến nào được gán giá trị.

Thí dụ:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

Tuy nhiên, Lập trình thủ tục sử dụng trạng thái.

Thí dụ:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
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.