Là mẫu phù hợp với các loại thành ngữ hoặc thiết kế kém?


18

Có vẻ như mã F # thường khớp mẫu với các loại. Chắc chắn

match opt with 
| Some val -> Something(val) 
| None -> Different()

có vẻ phổ biến

Nhưng từ góc độ OOP, trông rất giống luồng điều khiển dựa trên kiểm tra loại thời gian chạy, thường sẽ bị nhăn mặt. Để đánh vần nó, trong OOP có lẽ bạn thích sử dụng quá tải:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

Đây chắc chắn là nhiều mã hơn. OTOH, dường như tâm trí OOP-y của tôi có lợi thế về cấu trúc:

  • mở rộng cho một hình thức mới Tlà dễ dàng;
  • Tôi không phải lo lắng về việc tìm kiếm sự trùng lặp của luồng điều khiển chọn tuyến đường; và
  • lựa chọn tuyến đường là bất biến theo nghĩa là một khi tôi đã có Footrong tay, tôi không bao giờ phải lo lắng về Bar.Route()việc thực hiện

Có những lợi thế nào cho việc khớp mẫu với các loại mà tôi không thấy? Nó được coi là thành ngữ hay nó là một khả năng không được sử dụng phổ biến?


3
Làm thế nào nhiều ý nghĩa để xem một ngôn ngữ chức năng từ quan điểm OOP? Dù sao, sức mạnh thực sự của khớp mẫu đi kèm với các mẫu lồng nhau. Chỉ cần kiểm tra các nhà xây dựng ngoài cùng là có thể, nhưng không có nghĩa là toàn bộ câu chuyện.
Ingo

Điều này - But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.- nghe có vẻ quá giáo điều. Đôi khi, bạn muốn tách ops của mình khỏi hệ thống phân cấp của mình: có thể 1) bạn không thể thêm op vào hệ thống phân cấp b / c mà bạn không sở hữu hệ thống phân cấp; 2) các lớp bạn muốn có op không khớp với thứ bậc của bạn; 3) bạn có thể thêm op vào hệ thống phân cấp của mình, nhưng không muốn b / c bạn không muốn làm lộn xộn API phân cấp của mình với một loạt các crap mà hầu hết khách hàng không sử dụng.

4
Chỉ cần làm rõ, SomeNonekhông loại. Cả hai đều là các hàm tạo có kiểu forall a. a -> option aforall a. option a(xin lỗi, không chắc cú pháp cho chú thích kiểu là gì trong F #).

Câu trả lời:


20

Bạn đã đúng khi phân cấp lớp OOP có liên quan rất chặt chẽ với các hiệp hội bị phân biệt đối xử trong F # và việc khớp mẫu có liên quan rất chặt chẽ với các bài kiểm tra kiểu động. Trên thực tế, đây thực sự là cách F # biên dịch các hiệp hội phân biệt đối xử với .NET!

Về khả năng mở rộng, có hai mặt của vấn đề:

  • OO cho phép bạn thêm các lớp con mới, nhưng làm cho việc thêm các hàm (ảo) mới trở nên khó khăn
  • FP cho phép bạn thêm các chức năng mới, nhưng làm cho việc thêm các trường hợp kết hợp mới trở nên khó khăn

Điều đó nói rằng, F # sẽ đưa ra cảnh báo khi bạn bỏ lỡ một trường hợp khớp mẫu, vì vậy việc thêm các trường hợp kết hợp mới thực sự không tệ.

Về việc tìm kiếm các bản sao trong lựa chọn root - F # sẽ đưa ra cảnh báo khi bạn có một kết quả trùng lặp, ví dụ:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

Thực tế là "lựa chọn tuyến đường là bất biến" cũng có thể có vấn đề. Ví dụ: nếu bạn muốn chia sẻ việc thực hiện một hàm giữa FooBarcác trường hợp, nhưng làm một cái gì đó khác cho Zootrường hợp, bạn có thể mã hóa dễ dàng bằng cách sử dụng khớp mẫu:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

Nói chung, FP tập trung hơn vào việc thiết kế các loại đầu tiên và sau đó thêm các chức năng. Vì vậy, nó thực sự có lợi từ việc bạn có thể điều chỉnh các loại (mô hình miền) của mình trong một vài dòng trong một tệp và sau đó dễ dàng thêm các chức năng hoạt động trên mô hình miền.

Hai cách tiếp cận - OO và FP khá bổ sung và cả hai đều có những ưu điểm và nhược điểm. Điều khó khăn (đến từ phối cảnh OO) là F # thường sử dụng kiểu FP làm mặc định. Nhưng nếu thực sự cần thêm các lớp con mới, bạn luôn có thể sử dụng các giao diện. Nhưng trong hầu hết các hệ thống, bạn cần phải thêm các loại và chức năng như nhau, vì vậy sự lựa chọn thực sự không quan trọng lắm - và sử dụng các hiệp hội phân biệt đối xử trong F # sẽ đẹp hơn.

Tôi muốn giới thiệu loạt blog tuyệt vời này để biết thêm thông tin.


3
Mặc dù bạn đã đúng, tôi muốn nói thêm rằng đây không phải là vấn đề của OO so với FP vì đây là vấn đề của các đối tượng so với các loại tổng. Nỗi ám ảnh của OOP với họ sang một bên, không có gì về các đối tượng khiến chúng không hoạt động. Và nếu bạn nhảy qua đủ số vòng, bạn cũng có thể triển khai các loại tổng trong các ngôn ngữ OOP chính thống (mặc dù nó sẽ không đẹp).
Doval

1
"Và nếu bạn nhảy qua đủ số vòng, bạn cũng có thể triển khai các loại tổng trong các ngôn ngữ OOP chính thống (mặc dù nó sẽ không đẹp)." -> Tôi đoán bạn sẽ kết thúc với một cái gì đó tương tự như cách các loại F # sum được mã hóa trong hệ thống loại .NET :)
Tarmil

7

Bạn đã quan sát chính xác rằng khớp mẫu (về cơ bản là một câu lệnh chuyển đổi tăng áp) và công văn động có những điểm tương đồng. Họ cũng cùng tồn tại trong một số ngôn ngữ, với một kết quả rất thú vị. Tuy nhiên, có sự khác biệt nhỏ.

Tôi có thể sử dụng hệ thống loại để xác định loại chỉ có thể có một số kiểu con cố định:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

Sẽ không bao giờ có một kiểu con khác của Boolhoặc Option, vì vậy, phân lớp có vẻ không hữu ích (một số ngôn ngữ như Scala có một khái niệm về phân lớp có thể xử lý việc này - một lớp có thể được đánh dấu là lớp cuối cùng bên ngoài đơn vị biên dịch hiện tại, nhưng các kiểu con có thể được định nghĩa bên trong đơn vị biên dịch này).

Vì các kiểu con của một kiểu như Optionhiện được biết đến tĩnh , trình biên dịch có thể cảnh báo nếu chúng ta quên xử lý một trường hợp trong mẫu khớp của chúng ta. Điều này có nghĩa là một trận đấu mẫu giống như một trò hề đặc biệt buộc chúng ta phải xử lý tất cả các tùy chọn.

Hơn nữa, công văn phương thức động (được yêu cầu cho OOP) cũng ngụ ý kiểm tra loại thời gian chạy, nhưng thuộc loại khác. Do đó, điều này khá không liên quan nếu chúng ta thực hiện kiểm tra loại này một cách rõ ràng thông qua khớp mẫu hoặc ngầm định thông qua một cuộc gọi phương thức.


"Điều này có nghĩa là một trận đấu mẫu giống như một cơn mưa đặc biệt buộc chúng ta phải xử lý tất cả các tùy chọn" - thực tế, tôi tin rằng (miễn là bạn chỉ khớp với các nhà xây dựng và không phải là giá trị hoặc cấu trúc lồng nhau) thì nó là đẳng cấu đặt một phương thức ảo trừu tượng trong siêu lớp.
Jules

2

Việc khớp mẫu F # thường được thực hiện với một liên minh phân biệt hơn là với các lớp (và do đó về mặt kỹ thuật không phải là kiểm tra kiểu). Điều này cho phép trình biên dịch đưa ra cảnh báo khi bạn không xác định được các trường hợp trong khớp mẫu.

Một điều cần lưu ý là trong một kiểu chức năng, bạn sắp xếp mọi thứ theo chức năng chứ không phải theo dữ liệu, do đó, khớp mẫu cho phép bạn tập hợp các chức năng khác nhau vào một nơi thay vì phân tán giữa các lớp. Điều này cũng có lợi thế là bạn có thể thấy các trường hợp khác được xử lý ngay bên cạnh nơi bạn cần thực hiện các thay đổi của mình.

Thêm một tùy chọn mới sau đó trông như:

  1. Thêm một tùy chọn mới cho liên minh bị phân biệt đối xử của bạn
  2. Khắc phục tất cả các cảnh báo trên các mẫu không khớp

2

Một phần, bạn thấy nó thường xuyên hơn trong lập trình chức năng vì bạn sử dụng các loại để đưa ra quyết định thường xuyên hơn. Tôi nhận ra rằng bạn có thể chỉ chọn các ví dụ ít nhiều một cách ngẫu nhiên, nhưng OOP tương đương với ví dụ khớp mẫu của bạn sẽ thường trông giống như:

if (opt != null)
    opt.Something()
else
    Different()

Nói cách khác, việc sử dụng đa hình để tránh những thứ thông thường như kiểm tra null trong OOP là tương đối hiếm. Giống như một lập trình viên OO không tạo ra một đối tượng null trong mọi tình huống nhỏ, một lập trình viên chức năng không phải lúc nào cũng làm quá tải một chức năng, đặc biệt là khi bạn biết danh sách các mẫu của bạn được đảm bảo là đầy đủ. Nếu bạn sử dụng hệ thống loại trong nhiều tình huống, bạn sẽ thấy hệ thống được sử dụng theo những cách bạn không quen.

Ngược lại, lập trình chức năng thành ngữ tương đương với ví dụ OOP của bạn rất có thể sẽ không sử dụng khớp mẫu, nhưng sẽ có fooRoutevà các barRoutehàm sẽ được chuyển làm đối số cho mã gọi. Nếu ai đó sử dụng khớp mẫu trong tình huống đó, nó thường sẽ bị coi là sai, giống như ai đó chuyển sang loại sẽ bị coi là sai trong OOP.

Vì vậy, khi khớp mẫu được coi là mã lập trình chức năng tốt? Khi bạn đang làm nhiều hơn là chỉ nhìn vào các loại và khi mở rộng các yêu cầu sẽ không yêu cầu thêm nhiều trường hợp. Ví dụ, Some valkhông chỉ kiểm tra optloại có loại Some, nó cũng liên kết valvới loại cơ bản để sử dụng an toàn loại ở phía bên kia của ->. Bạn biết rằng rất có thể bạn sẽ không bao giờ có nhu cầu cho trường hợp thứ ba, vì vậy đó là một cách sử dụng tốt.

Kết hợp mẫu có thể rất giống với câu lệnh chuyển đổi hướng đối tượng, nhưng có nhiều thứ đang diễn ra, đặc biệt là với các mẫu dài hơn hoặc lồng nhau. Hãy chắc chắn rằng bạn đã tính đến mọi thứ mà nó đang làm trước khi tuyên bố nó tương đương với một số mã OOP được thiết kế kém. Thông thường, nó xử lý ngắn gọn một tình huống không thể được trình bày rõ ràng trong hệ thống phân cấp thừa kế.


Tôi biết bạn biết điều này và có lẽ nó đã trượt tâm trí của bạn trong khi viết câu trả lời của bạn, nhưng lưu ý rằng SomeNonekhông phải là loại, vì vậy bạn không khớp mẫu trên các loại. Bạn mô hình trận đấu trên nhà xây dựng của cùng loại . Điều này không giống như hỏi "instanceof".
Andres F.
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.