Các ngôn ngữ lập trình hàm hoạt động như thế nào?


92

Nếu các ngôn ngữ lập trình chức năng không thể lưu bất kỳ trạng thái nào, thì làm cách nào để chúng thực hiện những công việc đơn giản như đọc đầu vào từ người dùng? Làm cách nào để họ "lưu trữ" đầu vào (hoặc lưu trữ bất kỳ dữ liệu nào cho vấn đề đó?)

Ví dụ: làm thế nào mà thứ C đơn giản này lại dịch sang một ngôn ngữ lập trình chức năng như Haskell?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(Câu hỏi của tôi được truyền cảm hứng bởi bài đăng xuất sắc này: "Thực thi trong Vương quốc Danh từ" . Đọc nó giúp tôi hiểu rõ hơn về lập trình hướng đối tượng chính xác là gì, cách Java triển khai nó theo một cách cực đoan và cách ngôn ngữ lập trình hàm là một tương phản.)



4
Đó là một câu hỏi hay, bởi vì ở cấp độ hệ thống, một máy tính cần trạng thái để trở nên hữu ích. Tôi đã xem một cuộc phỏng vấn với Simon Peyton-Jones (một trong những nhà phát triển đằng sau Haskell), nơi anh ấy nói rằng một máy tính chỉ chạy phần mềm hoàn toàn không trạng thái chỉ có thể đạt được một điều: Trở nên nóng bỏng! Nhiều câu trả lời hay bên dưới. Có hai chiến lược chính: 1) Tạo ra một ngôn ngữ không tinh khiết. 2) Lập một kế hoạch xảo quyệt đến trạng thái trừu tượng, đó là những gì Haskell làm, về cơ bản là tạo ra một Thế giới mới, thay đổi một chút thay vì sửa đổi cái cũ.
hại

14
Không phải SPJ nói về tác dụng phụ ở đó, không phải trạng thái sao? Các phép tính thuần túy có rất nhiều trạng thái tiềm ẩn trong các ràng buộc đối số và ngăn xếp cuộc gọi, nhưng nếu không có các tác dụng phụ (ví dụ: I / O) thì không thể làm bất cứ điều gì hữu ích. Hai điểm thực sự khá khác biệt - có rất nhiều mã Haskell tinh khiết, trạng thái và Stateđơn nguyên rất thanh lịch; mặt khác IOlà một bản hack xấu xí, bẩn thỉu chỉ được sử dụng một cách miễn cưỡng.
CA McCann

4
camccann nói đúng. Có rất nhiều trạng thái trong các ngôn ngữ chức năng. Nó chỉ được quản lý rõ ràng thay vì "hành động ma quái ở khoảng cách xa" như trong các ngôn ngữ mệnh lệnh.
CHỈ LÀ Ý KIẾN chính xác của TÔI,

1
Có thể có một số nhầm lẫn ở đây. Có lẽ máy tính cần các hiệu ứng để trở nên hữu ích, nhưng tôi nghĩ câu hỏi ở đây là về ngôn ngữ lập trình chứ không phải máy tính.
Conal

Câu trả lời:


80

Nếu các ngôn ngữ lập trình chức năng không thể lưu bất kỳ trạng thái nào, thì làm cách nào để chúng thực hiện một số công việc đơn giản như đọc đầu vào từ người dùng (ý tôi là làm cách nào để chúng "lưu trữ" nó) hoặc lưu trữ bất kỳ dữ liệu nào cho vấn đề đó?

Như bạn đã thu thập, lập trình chức năng không có trạng thái — nhưng điều đó không có nghĩa là nó không thể lưu trữ dữ liệu. Sự khác biệt là nếu tôi viết một câu lệnh (Haskell) dọc theo dòng

let x = func value 3.14 20 "random"
in ...

Tôi đảm bảo rằng giá trị của xluôn giống nhau ở chỗ ...: không gì có thể thay đổi được. Tương tự, nếu tôi có một hàm f :: String -> Integer(một hàm lấy một chuỗi và trả về một số nguyên), tôi có thể chắc chắn rằng nó fsẽ không sửa đổi đối số của nó, hoặc thay đổi bất kỳ biến toàn cục nào hoặc ghi dữ liệu vào tệp, v.v. Như sepp2k đã nói trong một nhận xét ở trên, tính không thể thay đổi này thực sự hữu ích cho việc lập luận về các chương trình: bạn viết các hàm gập, trục chính và cắt xén dữ liệu của mình, trả về các bản sao mới để bạn có thể chuỗi chúng lại với nhau và bạn có thể chắc chắn rằng không của những lời gọi hàm có thể làm bất cứ điều gì "có hại". Bạn biết điều đó xluôn là như vậy x, và bạn không cần phải lo lắng rằng ai đó đã viếtx := foo bar ở đâu đó giữa tuyên bố củax và việc sử dụng nó, bởi vì điều đó là không thể.

Bây giờ, nếu tôi muốn đọc thông tin đầu vào từ người dùng thì sao? Như KennyTM đã nói, ý tưởng là một hàm không tinh khiết là một hàm thuần túy được truyền toàn bộ thế giới dưới dạng đối số và trả về cả kết quả của nó và thế giới. Tất nhiên, bạn không muốn thực sự làm điều này: đối với một điều, nó kỳ quặc kinh khủng, và đối với một điều khác, điều gì sẽ xảy ra nếu tôi sử dụng lại cùng một đối tượng thế giới? Vì vậy, điều này được trừu tượng hóa bằng cách nào đó. Haskell xử lý nó với loại IO:

main :: IO ()
main = do str <- getLine
          let no = fst . head $ reads str :: Integer
          ...

Điều này cho chúng ta biết rằng đó mainlà một hành động IO không trả về gì; thực hiện hành động này là ý nghĩa của việc chạy chương trình Haskell. Quy tắc là các loại IO không bao giờ có thể thoát khỏi một hành động IO; trong ngữ cảnh này, chúng tôi giới thiệu hành động đó bằng cách sử dụng do. Do đó, getLinetrả về một IO String, có thể được coi là theo hai cách: thứ nhất, như một hành động, khi chạy, tạo ra một chuỗi; thứ hai, là một chuỗi bị IO "làm bẩn" vì nó được lấy không thuần khiết. Cách đầu tiên đúng hơn, nhưng cách thứ hai có thể hữu ích hơn. Việc <-lấy Stringra IO Stringvà cất vào str— nhưng vì chúng ta đang trong một hành động IO, nên chúng ta sẽ phải bọc nó lại, vì vậy nó không thể "thoát ra". Dòng tiếp theo cố gắng đọc một số nguyên ( reads) và lấy kết quả khớp thành công đầu tiên (fst . head); cái này hoàn toàn là nguyên chất (không có IO), vì vậy chúng tôi đặt tên cho nó là let no = .... Sau đó chúng ta có thể sử dụng cả hai nostrtrong .... Do đó, chúng tôi đã lưu trữ dữ liệu không tinh khiết (từ getLinevào str) và dữ liệu thuần túy ( let no = ...).

Cơ chế làm việc với IO này rất mạnh mẽ: nó cho phép bạn tách phần thuần túy, thuật toán của chương trình khỏi phần không tinh khiết, tương tác với người dùng và thực thi điều này ở cấp loại. minimumSpanningTreeChức năng của bạn không thể thay đổi điều gì đó ở đâu đó khác trong mã của bạn hoặc viết thông báo cho người dùng của bạn, v.v. Nó an toàn.

Đây là tất cả những gì bạn cần biết để sử dụng IO trong Haskell; nếu đó là tất cả những gì bạn muốn, bạn có thể dừng ở đây. Nhưng nếu bạn muốn hiểu tại sao điều đó lại hiệu quả, hãy tiếp tục đọc. (Và lưu ý rằng nội dung này sẽ dành riêng cho Haskell — các ngôn ngữ khác có thể chọn cách triển khai khác.)

Vì vậy, điều này có vẻ như là một chút gian lận, bằng cách nào đó đã thêm tạp chất vào Haskell nguyên chất. Nhưng không phải vậy - hóa ra chúng ta có thể triển khai kiểu IO hoàn toàn trong Haskell thuần túy (miễn là chúng ta được cung cấp RealWorld). Ý tưởng là thế này: một hành động IO IO typegiống như một hàm RealWorld -> (type, RealWorld), lấy thế giới thực và trả về cả đối tượng kiểu typevà đối tượng đã sửa đổi RealWorld. Sau đó, chúng tôi xác định một vài hàm để chúng tôi có thể sử dụng kiểu này mà không bị điên:

return :: a -> IO a
return a = \rw -> (a,rw)

(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'

Điều đầu tiên cho phép chúng ta nói về các hành động IO không làm gì cả: return 3là một hành động IO không truy vấn thế giới thực và chỉ trả về 3. Các >>=nhà điều hành, phát âm là "ràng buộc", cho phép chúng ta chạy hành động IO. Nó trích xuất giá trị từ hành động IO, chuyển nó và thế giới thực thông qua hàm và trả về hành động IO kết quả. Lưu ý rằng >>=thực thi quy tắc của chúng tôi rằng kết quả của các hành động IO không bao giờ được phép thoát ra ngoài.

Sau đó, chúng ta có thể biến những điều trên mainthành một tập hợp các ứng dụng chức năng thông thường sau:

main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...

Bước nhảy thời gian chạy Haskell bắt đầu mainvới bước đầu tiên RealWorldvà chúng tôi đã thiết lập! Mọi thứ đều tinh khiết, nó chỉ có một cú pháp lạ mắt.

[ Chỉnh sửa: Như @Conal đã chỉ ra , đây thực sự không phải là những gì Haskell sử dụng để thực hiện IO. Mô hình này sẽ bị phá vỡ nếu bạn thêm đồng thời hoặc thực sự là bất kỳ cách nào để thế giới thay đổi giữa một hành động IO, vì vậy Haskell sẽ không thể sử dụng mô hình này. Nó chỉ chính xác cho tính toán tuần tự. Do đó, có thể IO của Haskell hơi né tránh; ngay cả khi nó không, nó chắc chắn không hoàn toàn thanh lịch. Theo quan sát của @ Conal, hãy xem Simon Peyton-Jones nói gì trong Tackling the Awesome Squad [pdf] , phần 3.1; anh ấy trình bày những gì có thể tương ứng với một mô hình thay thế dọc theo những dòng này, nhưng sau đó bỏ nó vì độ phức tạp của nó và thực hiện một cách giải quyết khác.]

Một lần nữa, điều này giải thích (khá nhiều) cách IO và khả năng thay đổi nói chung hoạt động trong Haskell; nếu đây là tất cả những gì bạn muốn biết, bạn có thể dừng đọc ở đây. Nếu bạn muốn một liều lý thuyết cuối cùng, hãy tiếp tục đọc — nhưng hãy nhớ rằng, tại thời điểm này, chúng tôi đã thực sự đi rất xa câu hỏi của bạn!

Vì vậy, điều cuối cùng: hóa ra cấu trúc này - một kiểu tham số với return>>=- rất chung chung; nó được gọi là đơn nguyên, doký hiệu return>>=hoạt động với bất kỳ đơn nguyên nào. Như bạn đã thấy ở đây, monads không phải là phép thuật; tất cả những gì kỳ diệu là docác khối biến thành các lệnh gọi hàm. Các RealWorldloại là nơi duy nhất chúng ta thấy bất kỳ ma thuật. Các loại như [], hàm tạo danh sách, cũng là monads và chúng không liên quan gì đến mã không tinh khiết.

Bây giờ bạn biết (gần như) mọi thứ về khái niệm đơn nguyên (ngoại trừ một số định luật phải được thỏa mãn và định nghĩa toán học chính thức), nhưng bạn thiếu trực giác. Có rất nhiều hướng dẫn đơn nguyên trực tuyến vô lý; Tôi thích cái này , nhưng bạn có các tùy chọn. Tuy nhiên, điều này có thể sẽ không giúp bạn ; cách thực sự duy nhất để có được trực giác là kết hợp sử dụng chúng và đọc một vài hướng dẫn vào đúng thời điểm.

Tuy nhiên, bạn không cần trực giác đó để hiểu IO . Hiểu một cách tổng quát các monads một cách tổng quát đang đóng băng, nhưng bạn có thể sử dụng IO ngay bây giờ. Bạn có thể sử dụng nó sau khi tôi chỉ cho bạn mainchức năng đầu tiên . Bạn thậm chí có thể coi mã IO như thể nó ở một ngôn ngữ không tinh khiết! Nhưng hãy nhớ rằng có một đại diện chức năng cơ bản: không ai gian lận.

(Tái bút: Xin lỗi về độ dài. Tôi đã đi hơi xa một chút.)


6
Điều khiến tôi luôn chú ý về Haskell (mà tôi đã thực hiện và đang nỗ lực hết sức để học hỏi) là sự xấu xí của cú pháp. Nó giống như họ lấy những thứ tồi tệ nhất của mọi ngôn ngữ khác, thả chúng vào một cái thùng và khuấy một cách tức giận. Và những người này sau đó sẽ phàn nàn về cú pháp kỳ lạ được thừa nhận (ở những nơi) của C ++!

19
Neil: Thật không? Tôi thực sự thấy cú pháp của Haskell rất rõ ràng. Tôi tò mò; cụ thể bạn đang đề cập đến cái gì? (Đối với những gì nó đáng giá, C ++ cũng không thực sự làm phiền tôi, ngoại trừ sự cần thiết phải làm > >trong các khuôn mẫu.)
Antal Spector-Zabusky

6
Đối với mắt tôi, mặc dù cú pháp của Haskell không rõ ràng như Scheme, nhưng nó không bắt đầu so sánh với cú pháp ghê tởm của, tốt, thậm chí là đẹp nhất trong các ngôn ngữ có dấu ngoặc nhọn, trong đó C ++ là một trong những ngôn ngữ tệ nhất . Tôi cho là không có lý do gì. Tôi không nghĩ rằng tồn tại một ngôn ngữ mà mọi người đều thấy hài lòng về mặt cú pháp.
CA McCann

8
@NeilButterworth: Tôi nghi ngờ vấn đề của bạn không liên quan nhiều đến cú pháp như tên hàm. Nếu các hàm giống >>=hoặc $có nhiều hơn mà thay vào đó được gọi bindapply, mã haskell sẽ trông giống perl hơn nhiều. Ý tôi là sự khác biệt chính giữa haskell và cú pháp lược đồ là haskell có các toán tử infix và các parens tùy chọn. Nếu mọi người không lạm dụng các toán tử infix, haskell sẽ trông giống như một lược đồ với ít parens hơn.
sepp2k

5
@camcann: Chà, ý tôi là: Cú pháp cơ bản của lược đồ là (functionName arg1 arg2). Nếu bạn loại bỏ các parens thì functionName arg1 arg2đó là cú pháp haskell. Nếu bạn cho phép các toán tử infix với những cái tên khủng khiếp tùy ý, bạn nhận được arg1 §$%&/*°^? arg2nó thậm chí còn giống haskell hơn. (Tôi chỉ đang trêu chọc btw, tôi thực sự thích haskell).
sepp2k

23

Rất nhiều câu trả lời hay ở đây, nhưng chúng dài. Tôi sẽ cố gắng đưa ra một câu trả lời ngắn gọn hữu ích:

  • Các ngôn ngữ chức năng đặt trạng thái ở những vị trí giống như C: trong các biến được đặt tên và trong các đối tượng được phân bổ trên heap. Sự khác biệt là:

    • Trong một ngôn ngữ chức năng, một "biến" nhận được giá trị ban đầu của nó khi nó nằm trong phạm vi (thông qua một lệnh gọi hàm hoặc phép liên kết) và giá trị đó không thay đổi sau đó . Tương tự, một đối tượng được cấp phát trên heap ngay lập tức được khởi tạo với các giá trị của tất cả các trường của nó, các giá trị này không thay đổi sau đó.

    • "Thay đổi trạng thái" được xử lý không phải bằng cách thay đổi các biến hoặc đối tượng hiện có mà bằng cách ràng buộc các biến mới hoặc cấp phát các đối tượng mới.

  • IO hoạt động bằng một thủ thuật. Một phép tính hiệu quả phụ tạo ra một chuỗi được mô tả bằng một hàm lấy Thế giới làm đối số và trả về một cặp chứa chuỗi và Thế giới mới. Thế giới bao gồm nội dung của tất cả các ổ đĩa, lịch sử của mọi gói mạng từng được gửi hoặc nhận, màu sắc của mỗi pixel trên màn hình và những thứ tương tự. Mấu chốt của thủ thuật là quyền truy cập vào Thế giới bị hạn chế cẩn thận để

    • Không chương trình nào có thể tạo ra một bản sao của Thế giới (bạn sẽ đặt nó ở đâu?)

    • Không chương trình nào có thể vứt bỏ Thế giới

    Sử dụng thủ thuật này sẽ giúp có một Thế giới duy nhất có trạng thái phát triển theo thời gian. Hệ thống thời gian chạy bằng ngôn ngữ, không được viết bằng ngôn ngữ chức năng, thực hiện tính toán hiệu quả bên bằng cách cập nhật Thế giới duy nhất tại chỗ thay vì trả lại một Thế giới mới.

    Thủ thuật này được Simon Peyton Jones và Phil Wadler giải thích rất hay trong bài báo mang tính bước ngoặt của họ "Lập trình chức năng bắt buộc" .


4
Theo như tôi có thể nói, IOcâu chuyện này ( World -> (a,World)) là một huyền thoại khi được áp dụng cho Haskell, vì mô hình đó chỉ giải thích tính toán tuần tự thuần túy, trong khi IOkiểu của Haskell bao gồm đồng thời. Theo "tuần tự thuần túy", ý tôi là ngay cả thế giới (vũ trụ) cũng không được phép thay đổi giữa điểm bắt đầu và điểm kết thúc của một phép tính bắt buộc, ngoại trừ việc tính toán đó. Ví dụ, trong khi máy tính của bạn đang hoạt động, bộ não của bạn không thể hoạt động. Tương tự có thể được xử lý bởi một cái gì đó giống hơn World -> PowerSet [(a,World)], cho phép không xác định và đan xen.
Mùa

1
@Conal: Tôi nghĩ câu chuyện IO khái quát khá độc đáo theo chủ nghĩa không xác định và đan xen; nếu tôi nhớ không lầm, có một lời giải thích khá hay trong bài báo "Biệt đội khó xử". Nhưng tôi không biết một bài báo tốt giải thích rõ ràng về sự song song thực sự.
Norman Ramsey

3
Theo tôi hiểu, bài báo "Biệt đội khó xử" từ bỏ nỗ lực tổng quát hóa mô hình biểu thị đơn giản IO, tức là World -> (a,World)("huyền thoại" phổ biến và dai dẳng mà tôi đã đề cập đến) và thay vào đó đưa ra giải thích hoạt động. Một số người thích ngữ nghĩa hoạt động, nhưng họ hoàn toàn không hài lòng với tôi. Vui lòng xem câu trả lời dài hơn của tôi trong một câu trả lời khác.
Mùa

+1 Điều này đã giúp tôi hiểu hơn về IO Monads cũng như trả lời câu hỏi.
CaptainCasey

Hầu hết các trình biên dịch Haskell thực sự định nghĩa IORealWorld -> (a,RealWorld), nhưng thay vì thực sự đại diện cho thế giới thực, nó chỉ là một giá trị trừu tượng phải được chuyển qua và cuối cùng được trình biên dịch tối ưu hóa.
Danh sách Jeremy

19

Tôi đang ngắt phần trả lời nhận xét cho câu trả lời mới, để có thêm không gian:

Tôi đã viết:

Theo như tôi có thể nói, IOcâu chuyện này ( World -> (a,World)) là một huyền thoại khi áp dụng cho Haskell, vì mô hình đó chỉ giải thích tính toán tuần tự thuần túy, trong khi IOkiểu của Haskell bao gồm đồng thời. Theo "tuần tự hoàn toàn", ý tôi là ngay cả thế giới (vũ trụ) cũng không được phép thay đổi giữa điểm bắt đầu và kết thúc của một phép tính bắt buộc, ngoài việc tính toán đó. Ví dụ, trong khi máy tính của bạn đang hoạt động, bộ não của bạn không thể hoạt động. Tương tự có thể được xử lý bằng một thứ gì đó giống hơn World -> PowerSet [(a,World)], cho phép không xác định và xen kẽ.

Norman đã viết:

@Conal: Tôi nghĩ câu chuyện IO khái quát khá độc đáo theo chủ nghĩa không xác định và đan xen; nếu tôi nhớ không lầm, có một lời giải thích khá hay trong bài báo "Biệt đội khó xử". Nhưng tôi không biết một bài báo tốt giải thích rõ ràng về sự song song thực sự.

@Norman: Khái quát theo nghĩa nào? Tôi gợi ý rằng mô hình biểu thị / giải thích thường được đưa ra, World -> (a,World)không khớp với Haskell IOvì nó không tính đến thuyết không xác định và đồng thời. Có thể có một mô hình phức tạp hơn phù hợp, chẳng hạn như World -> PowerSet [(a,World)], nhưng tôi không biết liệu một mô hình như vậy đã được thực hiện và hiển thị đầy đủ & nhất quán hay chưa. Cá nhân tôi nghi ngờ có thể tìm thấy một con quái vật như vậy, vì nó IOđược điền bởi hàng nghìn lệnh gọi API bắt buộc do FFI nhập. Và như vậy, IOlà hoàn thành mục đích của nó:

Vấn đề mở: IOđơn nguyên đã trở thành tội lỗi của Haskell. (Bất cứ khi nào chúng tôi không hiểu điều gì đó, chúng tôi sẽ ném nó vào đơn nguyên IO.)

(Từ bài nói chuyện POPL của Simon PJ Mặc áo sơ mi mặc áo sơ mi cài tóc: hồi tưởng về Haskell .)

Trong Phần 3.1 của Giải quyết Biệt đội Ngại ngùng , Simon chỉ ra những điều không hiệu quả type IO a = World -> (a, World), bao gồm "Cách tiếp cận không mở rộng tốt khi chúng ta thêm đồng thời". Sau đó, ông đề xuất một mô hình thay thế khả thi, và sau đó từ bỏ nỗ lực giải thích hàm ý, nói

Tuy nhiên, thay vào đó, chúng tôi sẽ áp dụng ngữ nghĩa hoạt động, dựa trên các cách tiếp cận tiêu chuẩn đối với ngữ nghĩa của phép tính quy trình.

Sự thất bại trong việc tìm kiếm một mô hình biểu thị chính xác và hữu ích là căn nguyên của lý do tại sao tôi thấy Haskell IO là một sự rời bỏ tinh thần và những lợi ích sâu sắc của cái mà chúng tôi gọi là "lập trình chức năng", hay cái mà Peter Landin đặt tên cụ thể hơn là "lập trình biểu thị" . Xem bình luận tại đây.


Cảm ơn câu trả lời dài hơn. Tôi nghĩ có lẽ tôi đã bị tẩy não bởi các lãnh chúa hoạt động mới của chúng tôi. Động cơ trái và động cơ phải, v.v. đã giúp chứng minh một số định lý hữu ích. Bạn đã thấy bất kỳ mô hình biểu tượng nào mà bạn thích giải thích cho thuyết không xác định và đồng thời chưa? Tôi không có.
Norman Ramsey

1
Tôi thích cách World -> PowerSet [World]nắm bắt rõ ràng chủ nghĩa không xác định và sự đồng thời kiểu đan xen. Định nghĩa miền này cho tôi biết rằng lập trình mệnh lệnh đồng thời chính thống (bao gồm cả Haskell) là khó thực hiện - theo nghĩa đen, phức tạp hơn theo cấp số nhân so với tuần tự. Tác hại to lớn mà tôi thấy trong IOthần thoại Haskell là che lấp đi sự phức tạp vốn có này, thúc đẩy việc lật đổ nó.
Mùa

Trong khi tôi thấy lý do tại sao World -> (a, World)bị hỏng, tôi không rõ tại sao thay thế World -> PowerSet [(a,World)]đúng mô hình đồng thời, v.v. Đối với tôi, điều đó có vẻ ngụ ý rằng các chương trình trong đó IOsẽ chạy trong một cái gì đó giống như đơn nguyên danh sách, áp dụng chính chúng cho mọi mục của tập hợp được trả về bằng IOhành động. Tôi đang thiếu gì?
Antal Spector-Zabusky

3
@Absz: Đầu tiên, mô hình đề xuất của tôi, World -> PowerSet [(a,World)]không đúng. Hãy thử World -> PowerSet ([World],a)thay thế. PowerSetđưa ra tập hợp các kết quả có thể có (không xác định). [World]là chuỗi các trạng thái trung gian (không phải đơn nguyên danh sách / không xác định), cho phép xen kẽ (lập lịch luồng). Và ([World],a)cũng không hoàn toàn đúng, vì nó cho phép truy cập atrước khi đi qua tất cả các trạng thái trung gian. Thay vào đó, hãy xác định sử dụng World -> PowerSet (Computation a)ở đâudata Computation a = Result a | Step World (Computation a)
Conal

Tôi vẫn không thấy vấn đề với World -> (a, World). Nếu Worldloại thực sự bao gồm tất cả thế giới, thì nó cũng bao gồm thông tin về tất cả các quá trình đang chạy đồng thời và cũng là 'hạt giống ngẫu nhiên' của tất cả các thuyết không xác định. Kết quả Worldlà một thế giới với thời gian tiên tiến và một số tương tác được thực hiện. Vấn đề thực sự duy nhất của mô hình này là nó quá chung chung và các giá trị của Worldkhông thể được xây dựng và thao tác.
Rotsor

17

Lập trình hàm bắt nguồn từ lambda Calculus. Nếu bạn thực sự muốn hiểu về lập trình chức năng, hãy xem http://worrydream.com/AlligatorEggs/

Đó là một cách "thú vị" để học Giải tích lambda và đưa bạn vào thế giới thú vị của lập trình Hàm!

Làm thế nào biết Lambda Calculus rất hữu ích trong lập trình hàm.

Vì vậy, Lambda Calculus là nền tảng cho nhiều ngôn ngữ lập trình trong thế giới thực như Lisp, Scheme, ML, Haskell,….

Giả sử chúng ta muốn mô tả một hàm thêm ba vào bất kỳ đầu vào nào để làm như vậy, chúng ta sẽ viết:

plus3 x = succ(succ(succ x)) 

Đọc “plus3 là một hàm, khi được áp dụng cho bất kỳ số x nào, sẽ mang lại giá trị kế thừa của số kế thừa của x”

Lưu ý rằng hàm thêm 3 vào bất kỳ số nào không cần được đặt tên là plus3; tên “plus3” chỉ là cách viết tắt thuận tiện để đặt tên cho hàm này

(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

Lưu ý rằng chúng tôi sử dụng biểu tượng lambda cho một hàm (tôi nghĩ nó trông giống như một con cá sấu Tôi đoán đó là ý tưởng cho trứng cá sấu đến từ đâu)

Biểu tượng lambda là Alligator (một hàm) và x là màu của nó. Bạn cũng có thể coi x như một đối số (các hàm Lambda Calculus thực sự chỉ giả sử có một đối số) phần còn lại bạn có thể coi nó là phần thân của hàm.

Bây giờ hãy xem xét phần trừu tượng:

g  λ f. (f (f (succ 0)))

Đối số f được sử dụng trong một vị trí hàm (trong một lệnh gọi). Chúng tôi gọi hàm bậc cao hơn của ga vì nó nhận một hàm khác làm đầu vào. Bạn có thể coi hàm khác gọi f là " trứng ". Bây giờ sử dụng hai chức năng hoặc " Alligators " mà chúng tôi đã tạo, chúng tôi có thể làm điều gì đó như sau:

(g plus3) =  f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

Nếu bạn để ý, bạn có thể thấy rằng λ f Alligator của chúng ta ăn λ x Alligator của chúng ta và sau đó là λ x Alligator và chết. Sau đó, cá sấu λ x của chúng ta được tái sinh trong trứng cá sấu λ f. Sau đó, quá trình lặp lại và λ x Alligator ở bên trái bây giờ ăn λ x Alligator khác ở bên phải.

Sau đó, bạn có thể sử dụng bộ quy tắc đơn giản này của " Alligators " ăn " Alligators " để thiết kế một ngữ pháp và do đó các ngôn ngữ lập trình hàm được sinh ra!

Vì vậy, bạn có thể thấy nếu bạn biết Lambda Calculus, bạn sẽ hiểu cách hoạt động của Ngôn ngữ hàm.


@tuckster: Tôi đã nghiên cứu giải tích lambda một số lần trước đây ... và vâng, bài báo của AlligatorEggs có ý nghĩa với tôi. Nhưng tôi không thể liên hệ điều đó với lập trình. Đối với tôi, ngay bây giờ, giải tích labda giống như một lý thuyết riêng biệt, chỉ có ở đó. Các khái niệm về phép tính lambda được sử dụng trong ngôn ngữ lập trình như thế nào?
Lazer

3
@eSKay: Haskell phép tính lambda, với một lớp cú pháp mỏng để làm cho nó trông giống một ngôn ngữ lập trình bình thường hơn. Các ngôn ngữ thuộc họ Lisp cũng rất giống với phép tính lambda không định kiểu, là những gì mà Trứng cá sấu đại diện. Bản thân giải tích Lambda về cơ bản là một ngôn ngữ lập trình tối giản, giống như một "hợp ngữ lập trình chức năng".
CA McCann

@eSKay: Tôi đã thêm một chút về cách nó liên quan với một số ví dụ. Tôi hi vọng cái này giúp được!
PJT

Nếu bạn định trừ câu trả lời của tôi, bạn có thể vui lòng để lại nhận xét về lý do tại sao để tôi có thể thử và cải thiện câu trả lời của mình. Cảm ơn bạn.
PJT

14

Kỹ thuật xử lý trạng thái trong Haskell rất đơn giản. Và bạn không cần phải hiểu monads để xử lý nó.

Trong một ngôn ngữ lập trình có trạng thái, bạn thường có một số giá trị được lưu trữ ở đâu đó, một số đoạn mã thực thi, và sau đó bạn có một giá trị mới được lưu trữ. Trong ngôn ngữ mệnh lệnh, trạng thái này chỉ nằm ở đâu đó "trong nền". Trong một ngôn ngữ hàm (thuần túy), bạn thực hiện điều này một cách rõ ràng, vì vậy bạn viết một cách rõ ràng hàm biến đổi trạng thái.

Vì vậy, thay vì có một số trạng thái kiểu X, bạn viết các hàm ánh xạ X thành X. Vậy là xong! Bạn chuyển từ suy nghĩ về trạng thái sang suy nghĩ về những thao tác bạn muốn thực hiện trên trạng thái. Sau đó, bạn có thể chuỗi các chức năng này lại với nhau và kết hợp chúng với nhau theo nhiều cách khác nhau để tạo ra toàn bộ chương trình. Tất nhiên, bạn không bị giới hạn chỉ ánh xạ X đến X. Bạn có thể viết các hàm để lấy các kết hợp dữ liệu khác nhau làm đầu vào và trả về các kết hợp khác nhau ở cuối.

Các đơn nguyên là một trong số rất nhiều công cụ giúp tổ chức việc này. Nhưng monads thực sự không phải là giải pháp cho vấn đề. Giải pháp là nghĩ về các phép biến đổi trạng thái thay vì trạng thái.

Điều này cũng hoạt động với I / O. Trên thực tế, những gì sẽ xảy ra là: thay vì nhận đầu vào từ người dùng với một số tương đương trực tiếp scanfvà lưu trữ nó ở đâu đó, thay vào đó bạn viết một hàm để cho biết bạn sẽ làm gì với kết quả scanfnếu bạn có, và sau đó chuyển chức năng của I / O API. Đó chính xác là những gì >>=bạn làm khi sử dụng IOđơn nguyên trong Haskell. Vì vậy, bạn không bao giờ cần phải lưu trữ kết quả của bất kỳ I / O nào ở bất kỳ đâu - bạn chỉ cần viết mã cho biết cách bạn muốn biến đổi nó.


8

(Một số ngôn ngữ chức năng cho phép các chức năng không tinh khiết.)

Đối với các ngôn ngữ chức năng thuần túy , tương tác trong thế giới thực thường được bao gồm dưới dạng một trong các đối số hàm, như sau:

RealWorld pureScanf(RealWorld world, const char* format, ...);

Các ngôn ngữ khác nhau có các chiến lược khác nhau để trừu tượng hóa thế giới khỏi lập trình viên. Haskell, ví dụ, sử dụng monads để ẩn worldđối số.


Nhưng bản thân phần thuần túy của ngôn ngữ chức năng đã là Turing hoàn chỉnh, có nghĩa là bất cứ điều gì có thể làm được trong C cũng có thể làm được trong Haskell. Sự khác biệt chính đối với ngôn ngữ mệnh lệnh là thay vì sửa đổi các trạng thái tại chỗ:

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

Bạn kết hợp phần sửa đổi vào một lời gọi hàm, thường biến các vòng lặp thành đệ quy:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}

Hoặc chỉ computeSumOfSquares min max = sum [x*x | x <- [min..max]];-)
fredoverflow

@Fred: Khả năng hiểu danh sách chỉ là một đường cú pháp (và sau đó bạn cần giải thích đơn nguyên Danh sách một cách chi tiết). Và làm thế nào để bạn thực hiện sum? Đệ quy vẫn cần thiết.
kennytm

3

Ngôn ngữ chức năng có thể lưu trạng thái! Họ thường chỉ khuyến khích hoặc buộc bạn phải rõ ràng về việc làm như vậy.

Ví dụ, hãy xem Haskell's State Monad .


9
Và hãy nhớ rằng không có gì về Statehoặc Monadđiều đó cho phép trạng thái, vì cả hai đều được định nghĩa dưới dạng các công cụ chức năng đơn giản, chung chung. Họ chỉ nắm bắt các mẫu có liên quan, vì vậy bạn không cần phải phát minh lại bánh xe quá nhiều.
Mùa


1

haskell:

main = do no <- readLn
          print (no + 1)

Tất nhiên, bạn có thể gán mọi thứ cho các biến trong ngôn ngữ hàm. Bạn chỉ không thể thay đổi chúng (vì vậy về cơ bản tất cả các biến đều là hằng số trong ngôn ngữ hàm).


@ sepp2k: tại sao, thay đổi chúng có tác hại gì?
Lazer

@eSKay nếu bạn không thể thay đổi các biến thì bạn biết rằng chúng luôn giống nhau. Điều này làm cho việc gỡ lỗi trở nên dễ dàng hơn, buộc bạn phải tạo các chức năng đơn giản hơn chỉ làm một việc duy nhất và rất tốt. Nó cũng giúp ích rất nhiều khi làm việc với đồng thời.
Henrik Hansen

9
@eSKay: Các lập trình viên chức năng tin rằng trạng thái có thể thay đổi tạo ra nhiều khả năng xuất hiện lỗi và khiến việc lập luận về hành vi của chương trình trở nên khó khăn hơn. Ví dụ, nếu bạn có một lệnh gọi hàm f(x)và bạn muốn xem giá trị của x là bao nhiêu, bạn chỉ cần đi đến nơi x được xác định. Nếu x có thể thay đổi, bạn cũng phải xem xét liệu có điểm nào mà x có thể bị thay đổi giữa định nghĩa và cách sử dụng của nó hay không (điều này không tầm thường nếu x không phải là biến cục bộ).
sepp2k

6
Nó không chỉ là các lập trình viên chức năng mà trạng thái có thể thay đổi và các tác dụng phụ không tin tưởng. Các đối tượng bất biến và phân tách lệnh / truy vấn được khá nhiều lập trình viên OO coi trọng và hầu hết mọi người đều nghĩ rằng các biến toàn cục có thể thay đổi là một ý tưởng tồi. Những ngôn ngữ như Haskell chỉ đưa ý tưởng xa hơn rằng hầu hết ...
CA McCann

5
@eSKay: Việc đột biến có hại không quá nhiều nhưng hóa ra nếu bạn đồng ý tránh đột biến thì việc viết mã theo mô-đun và có thể tái sử dụng trở nên dễ dàng hơn nhiều. Không có trạng thái có thể thay đổi được chia sẻ, việc ghép nối giữa các phần khác nhau của mã trở nên rõ ràng và việc hiểu và duy trì thiết kế của bạn dễ dàng hơn rất nhiều. John Hughes giải thích điều này tốt hơn tôi có thể; lấy bài báo của anh ấy Tại sao Các vấn đề Lập trình Chức năng .
Norman Ramsey
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.