Sử dụng hàng đợi để tách các chức năng / tránh gọi trực tiếp?


8

Một loại câu hỏi lập trình chức năng cho người mới ở đây:

Tôi đã đọc bản dịch của một số bài nói chuyện của Rich Hickey và trong một số bài nổi tiếng hơn của anh ấy, anh ấy khuyên bạn nên sử dụng hàng đợi như một cách thay thế để có chức năng gọi cho nhau. (Ví dụ: trong Thiết kế, Thành phần và Hiệu suấtĐơn giản Dễ dàng .)

Tôi không hiểu điều này, ở một số khía cạnh:

  1. Có phải anh ta đang nói về việc đưa dữ liệu vào hàng đợi và sau đó mỗi chức năng sử dụng nó? Vì vậy, thay vì chức năng gọi hàm B để thực hiện tính toán riêng của mình, chúng ta chỉ có chức năng B tát đầu ra của nó trên hàng đợi và sau đó có chức năng A lấy nó? Hoặc, thay vào đó, chúng ta đang nói về việc đưa các hàm lên hàng đợi và sau đó áp dụng chúng liên tục vào dữ liệu (chắc chắn là không, vì điều đó sẽ liên quan đến đột biến lớn, phải không? Và cũng nhân lên hàng đợi cho các hàm đa năng, hoặc giống như cây hoặc cái gì đó? )

  2. Làm thế nào mà làm cho mọi thứ đơn giản hơn? Trực giác của tôi sẽ là chiến lược này sẽ tạo ra sự phức tạp hơn, bởi vì hàng đợi sẽ là một loại trạng thái, và sau đó bạn phải lo lắng "nếu một số chức năng khác lẻn vào và đặt một số dữ liệu lên hàng đợi thì sao?"

Một câu trả lời cho câu hỏi triển khai trên SO cho thấy ý tưởng này đang tạo ra một loạt các hàng đợi khác nhau. Vì vậy, mỗi hàm đặt đầu ra của nó trong hàng đợi riêng (??). Nhưng điều đó cũng làm tôi bối rối, bởi vì nếu bạn đang chạy một hàm một lần, thì tại sao nó lại cần một hàng đợi cho đầu ra của nó khi bạn có thể lấy đầu ra đó và đặt một tên trên đó là một (var, nguyên tử, nhập lớn bảng băm, bất cứ điều gì). Ngược lại, nếu một chức năng đang chạy nhiều lần và bạn dán đầu ra của nó vào hàng đợi, thì bạn lại tự gây ra trạng thái cho mình và bạn phải lo lắng về thứ tự mà mọi thứ được gọi, các hàm hạ lưu trở nên kém tinh khiết hơn, Vân vân.

Rõ ràng tôi không hiểu vấn đề ở đây. Ai đó có thể giải thích một chút?


Tôi không thấy một tài liệu tham khảo nào cho hàng đợi trong liên kết đầu tiên của bạn, mặc dù tôi sẽ cấp cho bạn rằng bài đăng đó cực kỳ dài và tôi có thể đã bỏ lỡ nó. Có vẻ như nó là một cuộc nói chuyện về nghệ thuật hơn là về lập trình.
Robert Harvey

Hàng đợi được đề cập ngắn gọn hai lần trong bài viết thứ hai, nhưng không được giải thích. Trong mọi trường hợp, ý tưởng sử dụng hàng đợi nhắn tin để liên lạc giữa các ứng dụng hoặc mô-đun đã xuất hiện trong một thời gian. Có vẻ như bạn sẽ không làm điều này trong một ứng dụng trừ khi bạn đang tạo một đường ống xử lý hoặc công cụ trạng thái.
Robert Harvey

Đó là trong đoạn dưới slide có tiêu đề "Tách thời gian / trật tự / dòng chảy" ("Bạn có thể phá vỡ các hệ thống, do đó, ít cuộc gọi trực tiếp hơn. Bạn có thể sử dụng hàng đợi để làm điều đó.")
Paul Gowder

3
Không đáng để đưa ra câu trả lời đầy đủ, nhưng anh ấy thực sự chỉ đưa ra một mô tả mờ nhạt cho khái niệm nhóm công việc và lập trình hướng sự kiện. Vì vậy, bạn đóng gói một lệnh gọi hàm vào một Jobđối tượng chung , đẩy nó vào hàng đợi và có một hoặc nhiều luồng worker hoạt động trên hàng đợi đó. Sau Jobđó gửi thêm Jobs vào hàng đợi sau khi hoàn thành. Giá trị trả về được thay thế bằng các cuộc gọi lại trong khái niệm đó. Đó là một cơn ác mộng để gỡ lỗi và xác minh khi bạn thiếu ngăn xếp cuộc gọi, và hiệu quả và linh hoạt vì cùng một lý do.
Ext3h

Cảm ơn. Có lẽ vấn đề thực sự của tôi là tôi không hiểu nhắn tin! (Heh, thời gian để đi học smalltalk? :-))
Paul Gowder

Câu trả lời:


6

Đó là một bài tập thiết kế nhiều hơn là một khuyến nghị chung. Bạn thường không đặt một hàng đợi giữa tất cả các cuộc gọi chức năng trực tiếp của bạn. Điều đó sẽ là vô lý. Tuy nhiên, nếu bạn không thiết kế các chức năng của mình như thể một hàng đợi có thể được chèn giữa bất kỳ lệnh gọi hàm trực tiếp nào, bạn không thể xác nhận chính xác rằng bạn đã viết mã có thể sử dụng lại và có thể ghép lại được. Đó là điểm mà Rich Hickey đang thực hiện.

Đây là một lý do chính đằng sau sự thành công của Apache Spark , ví dụ. Bạn viết mã trông giống như nó thực hiện các cuộc gọi hàm trực tiếp trên các bộ sưu tập cục bộ và khung dịch chuyển mã đó thành các thông điệp trên hàng đợi giữa các nút cụm. Kiểu mã hóa đơn giản, có thể ghép lại, có thể tái sử dụng mà Rich Hickey ủng hộ làm cho điều đó trở nên khả thi.


Nhưng đó không phải là một sự thay đổi trong quy trình ràng buộc phương thức sao? Vào cuối ngày, một cuộc gọi chức năng chỉ là một cuộc gọi chức năng, phải không? Điều gì xảy ra sau đó phụ thuộc vào chức năng làm gì. Vì vậy, có vẻ như ít thực hiện các cuộc gọi chức năng hơn là về cách cơ sở hạ tầng bên dưới được thiết kế.
Robert Harvey

1
Nói cách khác, bạn sẽ thực hiện thay đổi gì đối với các lệnh gọi hàm để làm cho chúng "thân thiện với hàng đợi?"
Robert Harvey

5
Hãy nhớ rằng, mã ở đầu kia của hàng đợi không nhất thiết phải có quyền truy cập vào cùng bộ nhớ và IO. Các chức năng thân thiện với hàng đợi không có tác dụng phụ và mong đợi các đầu vào và tạo ra các đầu ra không thay đổi và dễ dàng tuần tự hóa. Đó không phải là một thử nghiệm dễ dàng để đáp ứng trên nhiều cơ sở mã.
Karl Bielefeldt

3
À, vậy "lập trình chức năng thân thiện" rồi. Kinda có ý nghĩa, vì đó là Rich Hickey thảo luận về nó.
Robert Harvey

0

Một điều cần lưu ý là lập trình chức năng cho phép bạn kết nối các chức năng với nhau một cách gián tiếp thông qua các đối tượng hòa giải , đảm nhiệm việc mua các đối số để đưa vào các hàm và định tuyến linh hoạt kết quả của chúng cho người nhận muốn kết quả của chúng. Vì vậy, giả sử bạn có một số mã gọi trực tiếp đơn giản giống như ví dụ này, trong Haskell:

myThing :: A -> B -> C
myThing a b = f a (g a b)

Chà, bằng cách sử dụng Applicativelớp của Haskell và các toán tử của nó <$><*>chúng ta có thể viết lại mã đó một cách cơ học:

myThing :: Applicative f => f A -> f B -> f C
myThing a b = f <$> a <*> (g <$> a <*> b)

... ở đâu bây giờ myThingkhông còn trực tiếp gọi fg, nhưng thay vì kết nối chúng thông qua một số trung gian của loại f. Vì vậy, ví dụ, fcó thể là một số Streamloại được cung cấp bởi thư viện cung cấp giao diện cho hệ thống xếp hàng, trong trường hợp đó chúng ta sẽ có loại này:

myThing :: Stream A -> Stream B -> Stream C
myThing a b = f <$> a <*> (g <$> a <*> b)

Các hệ thống như thế này tồn tại. Trong thực tế, bạn có thể xem các luồng Java 8 như một phiên bản của mô hình này. Bạn nhận được mã như thế này:

List<Integer> transactionsIds = 
    transactions.parallelStream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

Ở đây bạn đang sử dụng các chức năng sau:

  • t -> t.getType() == Transaction.GROCERY
  • comparing(Transaction::getValue).reversed()
  • Transaction::getId
  • toList()

... và thay vì để họ gọi trực tiếp cho nhau, bạn đang sử dụng Streamhệ thống để hòa giải giữa họ. Ví dụ mã này không gọi Transaction::getIdhàm trực tiếp, đó Streamlà gọi nó với các giao dịch tồn tại trước đó filter. Bạn có thể nghĩ về Streammột hàng đợi rất tối thiểu mà các cặp đôi hoạt động gián tiếp và định tuyến các giá trị giữa chúng.

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.