Là hợp đồng ngữ nghĩa của một giao diện (OOP) có nhiều thông tin hơn chữ ký hàm (FP)?


16

Một số người nói rằng nếu bạn đưa các nguyên tắc RẮN đến cực đoan của họ, bạn sẽ kết thúc chương trình chức năng . Tôi đồng ý với bài viết này nhưng tôi nghĩ rằng một số ngữ nghĩa bị mất trong quá trình chuyển đổi từ giao diện / đối tượng sang chức năng / đóng cửa và tôi muốn biết làm thế nào Lập trình chức năng có thể giảm thiểu tổn thất.

Từ bài viết:

Hơn nữa, nếu bạn áp dụng nghiêm ngặt Nguyên tắc phân chia giao diện (ISP), bạn sẽ hiểu rằng bạn nên ưu tiên Giao diện vai trò trên Giao diện tiêu đề.

Nếu bạn tiếp tục hướng thiết kế của mình tới các giao diện nhỏ hơn và nhỏ hơn, cuối cùng bạn sẽ đến Giao diện vai trò cuối cùng: một giao diện với một phương thức duy nhất. Điều này xảy ra với tôi rất nhiều. Đây là một ví dụ:

public interface IMessageQuery
{
    string Read(int id);
}

Nếu tôi phụ thuộc vào một IMessageQuery, một phần của hợp đồng ngầm là việc gọi điện Read(id)sẽ tìm kiếm và trả lại một tin nhắn với ID đã cho.

So sánh điều này với việc phụ thuộc vào chữ ký chức năng tương đương của nó , int -> string. Không có bất kỳ tín hiệu bổ sung, chức năng này có thể là một đơn giản ToString(). Nếu bạn thực hiện IMessageQuery.Read(int id)với một ToString()tôi có thể buộc tội bạn đã cố tình lật đổ!

Vì vậy, các lập trình viên chức năng có thể làm gì để bảo tồn ngữ nghĩa của một giao diện có tên? Có phải thông thường, ví dụ, tạo một loại bản ghi với một thành viên không?

type MessageQuery = {
    Read: int -> string
}

5
Một giao diện OOP là giống như FP typeclass , không phải là một chức năng duy nhất.
9000

2
Bạn có thể có quá nhiều điều tốt, việc áp dụng ISP 'một cách nghiêm ngặt' để kết thúc với 1 phương thức cho mỗi giao diện chỉ là quá xa.
gbjbaanb

3
@gbjbaanb thực sự phần lớn các giao diện của tôi chỉ có một phương thức với nhiều triển khai. Bạn càng áp dụng các nguyên tắc RẮN, bạn càng có thể thấy các lợi ích. Nhưng đó là ngoài chủ đề cho câu hỏi này
AlexFoxGill

1
@jk.: Chà, ở Haskell, đó là các lớp, trong OCaml, bạn có thể sử dụng một mô-đun hoặc functor, trong Clojure bạn sử dụng một giao thức. Trong mọi trường hợp, bạn thường không giới hạn sự tương tự giao diện của mình với một chức năng duy nhất.
9000

2
Without any additional clues... Có lẽ đó là lý do tại sao tài liệu là một phần của hợp đồng ?
SJuan76

Câu trả lời:


8

Như Telastyn nói, so sánh các định nghĩa tĩnh của các hàm:

public string Read(int id) { /*...*/ }

đến

let read (id:int) = //...

Bạn chưa thực sự mất bất cứ thứ gì từ OOP đến FP.

Tuy nhiên, đây chỉ là một phần của câu chuyện, bởi vì các chức năng và giao diện không chỉ được đề cập trong các định nghĩa tĩnh của chúng. Họ cũng đi qua xung quanh . Vì vậy, hãy nói rằng chúng tôi MessageQueryđã được đọc bởi một đoạn mã khác, a MessageProcessor. Sau đó chúng tôi có:

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

Bây giờ chúng ta không thể trực tiếp nhìn thấy tên phương thức IMessageQuery.Readhoặc tham số của nó int id, nhưng chúng ta có thể đến đó rất dễ dàng thông qua IDE của chúng ta. Tổng quát hơn, thực tế là chúng ta chuyển một IMessageQuerygiao diện chứ không phải bất kỳ giao diện nào có phương thức một hàm từ int sang chuỗi có nghĩa là chúng ta giữ idsiêu dữ liệu tên tham số đó được liên kết với hàm này.

Mặt khác, đối với phiên bản chức năng của chúng tôi, chúng tôi có:

let read (id:int) (messageReader : int -> string) = // ...

Vì vậy, những gì chúng ta đã giữ và mất? Chà, chúng ta vẫn có tên tham số messageReader, có thể làm cho tên loại (tương đương IMessageQuery) không cần thiết. Nhưng bây giờ chúng tôi đã mất tên tham số idtrong chức năng của chúng tôi.


Có hai cách chính xung quanh điều này:

  • Đầu tiên, từ việc đọc chữ ký đó, bạn đã có thể đoán khá chính xác những gì sẽ xảy ra. Bằng cách giữ các chức năng ngắn, đơn giản và gắn kết và sử dụng cách đặt tên tốt, bạn sẽ dễ dàng hơn trong việc tìm kiếm hoặc tìm thông tin này. Khi chúng ta đã đọc được chức năng thực tế, nó sẽ còn đơn giản hơn nữa.

  • Thứ hai, nó được coi là thiết kế thành ngữ trong nhiều ngôn ngữ chức năng để tạo ra các loại nhỏ để bọc nguyên thủy. Trong trường hợp này, điều ngược lại đang xảy ra - thay vì thay thế tên loại bằng tên tham số ( IMessageQueryđến messageReader), chúng ta có thể thay thế tên tham số bằng tên loại. Ví dụ, intcó thể được bọc trong một loại được gọi là Id:

    type Id = Id of int
    

    Bây giờ readchữ ký của chúng tôi trở thành:

    let read (id:int) (messageReader : Id -> string) = // ...
    

    Đó chỉ là thông tin như những gì chúng ta đã có trước đây.

    Là một lưu ý phụ, điều này cũng cung cấp cho chúng tôi một số bảo vệ trình biên dịch mà chúng tôi có trong OOP. Trong khi phiên bản OOP đảm bảo chúng tôi thực hiện một cách cụ thể IMessageQuerychứ không phải bất kỳ int -> stringchức năng cũ nào , thì ở đây chúng tôi có một sự bảo vệ tương tự (nhưng khác biệt) mà chúng tôi đang thực hiện Id -> stringthay vì bất kỳ chức năng cũ nào int -> string.


Tôi miễn cưỡng nói 100% rằng các kỹ thuật này sẽ luôn tốt và có nhiều thông tin như có đầy đủ thông tin có sẵn trên một giao diện, nhưng tôi nghĩ từ các ví dụ trên, bạn có thể nói rằng hầu hết thời gian, chúng tôi có lẽ có thể làm tốt như một công việc


1
Đây là câu trả lời yêu thích của tôi - rằng FP khuyến khích sử dụng các loại ngữ nghĩa
AlexFoxGill

10

Khi làm FP tôi có xu hướng sử dụng các loại ngữ nghĩa cụ thể hơn.

Ví dụ, phương pháp của bạn đối với tôi sẽ trở thành một cái gì đó như:

read: MessageId -> Message

Này liên lạc khá nhiều hơn so với OO (/ java) kiểu ThingDoer.doThing()phong cách


2
+1. Ngoài ra, bạn có thể mã hóa nhiều thuộc tính hơn vào hệ thống loại bằng các ngôn ngữ FP được gõ như Haskell. Nếu đó là một loại haskell, tôi sẽ biết rằng nó hoàn toàn không thực hiện bất kỳ IO nào (và do đó có khả năng tham chiếu một số ánh xạ trong bộ nhớ của id tới các tin nhắn ở đâu đó). Trong các ngôn ngữ OOP, tôi không có thông tin đó - chức năng này có thể gọi cơ sở dữ liệu như là một phần của việc gọi nó.
Jack

1
Tôi không rõ chính xác lý do tại sao điều này sẽ giao tiếp nhiều hơn kiểu Java. Điều gì read: MessageId -> Messagenói với bạn rằng string MessageReader.GetMessage(int messageId)không?
Ben Aaronson

@BenAaronson tỷ lệ tín hiệu / nhiễu là tốt hơn. Nếu đó là một phương thức OO, tôi không biết nó đang làm gì dưới mui xe, thì nó có thể có bất kỳ loại phụ thuộc nào không được thể hiện. Như Jack đề cập, khi bạn có loại mạnh hơn, bạn có nhiều sự đảm bảo hơn.
Daenyth

9

Vì vậy, các lập trình viên chức năng có thể làm gì để bảo tồn ngữ nghĩa của một giao diện có tên?

Sử dụng các chức năng được đặt tên tốt.

IMessageQuery::Read: int -> stringchỉ đơn giản là trở thành ReadMessageQuery: int -> stringhoặc một cái gì đó tương tự.

Điều quan trọng cần lưu ý là tên chỉ là hợp đồng theo nghĩa lỏng lẻo nhất của từ này. Chúng chỉ hoạt động nếu bạn và một lập trình viên khác suy ra cùng một hàm ý từ tên và tuân theo chúng. Bởi vì điều này, bạn thực sự có thể sử dụng bất kỳ tên nào truyền đạt hành vi ngụ ý đó. OO và lập trình chức năng có tên của chúng ở những nơi hơi khác nhau và có hình dạng hơi khác nhau, nhưng chức năng của chúng là như nhau.

Là hợp đồng ngữ nghĩa của một giao diện (OOP) có nhiều thông tin hơn chữ ký hàm (FP)?

Không phải trong ví dụ này. Như tôi đã giải thích ở trên, một lớp / giao diện duy nhất với một chức năng duy nhất không có nhiều thông tin hơn một chức năng độc lập có tên tương tự.

Khi bạn nhận được nhiều hơn một chức năng / trường / thuộc tính trong một lớp, bạn có thể suy luận thêm thông tin về chúng vì bạn có thể thấy mối quan hệ của chúng. Không thể tranh cãi nếu đó là nhiều thông tin hơn các hàm độc lập có cùng tham số / tương tự hoặc các hàm độc lập được tổ chức bởi không gian tên hoặc mô-đun.

Cá nhân, tôi không nghĩ rằng OO có nhiều thông tin hơn đáng kể, ngay cả trong các ví dụ phức tạp hơn.


2
Tên tham số thì sao?
Ben Aaronson

@BenAaronson - còn họ thì sao? Cả OO và ngôn ngữ chức năng cho phép bạn chỉ định tên tham số, vì vậy đó là một cú đẩy. Tôi chỉ sử dụng cách viết tắt chữ ký loại ở đây để phù hợp với câu hỏi. Trong một ngôn ngữ thực, nó sẽ trông giống nhưReadMessageQuery id = <code to go fetch based on id>
Telastyn

1
Chà, nếu một hàm lấy một giao diện làm tham số, sau đó gọi một phương thức trên giao diện đó, tên tham số cho phương thức giao diện là có sẵn. Nếu một hàm lấy một hàm khác làm tham số, việc gán tên tham số cho hàm được truyền trong hàm là không dễ dàng
Ben Aaronson

1
Có, @BenAaronson đây là một trong những thông tin bị mất trong chữ ký hàm. Ví dụ trong let consumer (messageQuery : int -> string) = messageQuery 5, idtham số chỉ là một int. Tôi đoán một đối số là bạn nên vượt qua Idchứ không phải một int. Trong thực tế đó sẽ làm cho một câu trả lời tốt trong chính nó.
AlexFoxGill

@AlexFoxGill Thật ra tôi chỉ đang trong quá trình viết một cái gì đó dọc theo những dòng đó!
Ben Aaronson

0

Tôi không đồng ý rằng một chức năng duy nhất không thể có "hợp đồng ngữ nghĩa". Hãy xem xét các luật này cho foldr:

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

Theo nghĩa nào đó không phải là ngữ nghĩa, hoặc không phải là một hợp đồng? Bạn không cần phải xác định loại cho 'thư mục', đặc biệt vì foldrđược xác định duy nhất bởi các luật đó. Bạn biết chính xác những gì nó sẽ làm.

Nếu bạn muốn thực hiện một chức năng của một số loại, bạn có thể làm điều tương tự:

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

Bạn chỉ cần đặt tên và nắm bắt loại đó nếu bạn cần cùng một hợp đồng nhiều lần:

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

Trình kiểm tra loại sẽ không thực thi bất kỳ ngữ nghĩa nào mà bạn gán cho một loại, do đó, việc tạo một loại mới cho mọi hợp đồng chỉ là bản tóm tắt.


1
Tôi không nghĩ rằng điểm đầu tiên của bạn giải quyết câu hỏi - đó là định nghĩa hàm, không phải chữ ký. Tất nhiên, bằng cách xem xét việc triển khai một lớp hoặc một chức năng, bạn có thể biết nó làm gì - câu hỏi yêu cầu bạn vẫn có thể bảo tồn ngữ nghĩa ở mức độ trừu tượng (một giao diện hoặc chữ ký hàm)
AlexFoxGill

@AlexFoxGill - đó không phải là một định nghĩa hàm, mặc dù nó xác định duy nhất foldr. Gợi ý: định nghĩa foldrcó hai phương trình (tại sao?) Trong khi đặc tả được đưa ra ở trên có ba.
Jonathan Cast

Ý tôi là Foldr là một hàm không phải là chữ ký hàm. Các luật hoặc định nghĩa không phải là một phần của chữ ký
AlexFoxGill

-1

Khá nhiều tất cả các ngôn ngữ chức năng được gõ tĩnh có một cách để bí danh các loại cơ bản theo cách yêu cầu bạn khai báo rõ ràng ý định ngữ nghĩa của bạn. Một số câu trả lời khác đã đưa ra ví dụ. Trong thực tế, các lập trình viên chức năng có kinh nghiệm cần những lý do rất chính đáng để sử dụng các loại trình bao bọc đó, bởi vì chúng làm tổn thương khả năng kết hợp và tái sử dụng.

Ví dụ: giả sử khách hàng muốn triển khai truy vấn thư được hỗ trợ bởi danh sách. Trong Haskell, việc thực hiện có thể đơn giản như sau:

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

Sử dụng newtype Message = Message Stringđiều này sẽ ít đơn giản hơn nhiều, khiến cho việc thực hiện này trông giống như:

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

Điều đó có vẻ không phải là một vấn đề lớn, nhưng bạn hoặc phải thực hiện chuyển đổi kiểu đó qua lại ở mọi nơi hoặc thiết lập một lớp ranh giới trong mã của bạn, nơi mọi thứ ở trên là Int -> Stringsau đó bạn chuyển đổi nó Id -> Messageđể chuyển sang lớp bên dưới. Nói rằng tôi muốn thêm quốc tế hóa, hoặc định dạng nó trong tất cả các mũ, hoặc thêm bối cảnh đăng nhập, hoặc bất cứ điều gì. Những thao tác này đều đơn giản để kết hợp với một Int -> Stringvà gây khó chịu với một Id -> Message. Không phải là không bao giờ có trường hợp hạn chế loại tăng là mong muốn, nhưng sự phiền toái tốt hơn đáng để đánh đổi.

Bạn có thể sử dụng từ đồng nghĩa loại thay vì trình bao bọc (trong Haskell typethay vì newtype), điều này phổ biến hơn nhiều và không yêu cầu chuyển đổi ở mọi nơi, nhưng nó không cung cấp đảm bảo loại tĩnh như phiên bản OOP của bạn, chỉ là một chút của sự bao bọc. Kiểu bao bọc hầu hết được sử dụng trong trường hợp khách hàng không mong muốn thao túng giá trị, chỉ cần lưu trữ và chuyển lại. Ví dụ, một tập tin xử lý.

Không có gì có thể ngăn khách hàng bị "lật đổ". Bạn chỉ đang tạo ra các vòng để nhảy qua, cho mỗi khách hàng. Một giả đơn giản cho một bài kiểm tra đơn vị thông thường thường yêu cầu hành vi kỳ lạ không có ý nghĩa trong sản xuất. Giao diện của bạn nên được viết để họ không quan tâm, nếu có thể.


1
Cảm giác này giống như một nhận xét về câu trả lời của Ben / Daenyth - rằng bạn có thể sử dụng các loại ngữ nghĩa nhưng bạn không vì sự bất tiện này? Tôi đã không đánh giá thấp bạn nhưng nó không phải là một câu trả lời cho câu hỏi này.
AlexFoxGill

-1 Nó hoàn toàn không ảnh hưởng đến khả năng kết hợp hoặc tái sử dụng. Nếu IdMessagelà các hàm bao đơn giản cho IntString, việc chuyển đổi giữa chúng là chuyện nhỏ .
Michael Shaw

Vâng, nó tầm thường, nhưng vẫn khó chịu để làm tất cả mọi nơi. Tôi đã thêm một ví dụ và một chút mô tả sự khác biệt giữa việc sử dụng từ đồng nghĩa loại và trình bao bọc.
Karl Bielefeldt

Điều này có vẻ như một điều kích thước. Trong các dự án nhỏ, các loại trình bao bọc này có thể không hữu ích. Trong các dự án lớn, điều tự nhiên là các ranh giới sẽ chiếm một tỷ lệ nhỏ trong tổng thể mã, do đó, thực hiện các loại chuyển đổi này ở ranh giới và sau đó chuyển qua các loại được bọc ở mọi nơi khác không quá khó khăn. Ngoài ra, như Alex đã nói, tôi không thấy đây là câu trả lời cho câu hỏi như thế nào.
Ben Aaronson

Tôi đã giải thích làm thế nào để làm điều đó, tại sao các lập trình viên chức năng sẽ không làm điều đó. Đó là một câu trả lời, ngay cả khi đó không phải là người mà họ muốn. Nó không có gì để làm với kích thước. Ý tưởng này bằng cách nào đó là một điều tốt để cố tình làm cho việc sử dụng lại mã của bạn trở nên khó khăn hơn quyết định là một khái niệm OOP. Tạo một loại sản phẩm có thêm một số giá trị? Tất cả thời gian. Tạo một loại chính xác như một loại được sử dụng rộng rãi ngoại trừ bạn chỉ có thể sử dụng nó trong một thư viện? Thực tế chưa từng nghe thấy, ngoại trừ có lẽ trong mã FP tương tác nhiều với mã OOP hoặc được viết bởi một người chủ yếu quen thuộc với các thành ngữ OOP.
Karl Bielefeldt
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.