Vấn đề nào làm các loại dữ liệu đại số giải quyết?


18

Cảnh báo công bằng, tôi chưa quen với lập trình chức năng nên tôi có thể có nhiều giả định xấu.

Tôi đã học về các loại đại số. Nhiều ngôn ngữ chức năng dường như có chúng, và chúng khá hữu ích khi kết hợp với khớp mẫu. Tuy nhiên, vấn đề gì họ thực sự giải quyết? Tôi có thể triển khai một loại đại số dường như (sắp xếp) trong C # như thế này:

public abstract class Option { }
public class None : Option { }
public class Some<T> : Option
{
    public T Value { get; set; }
}

var result = GetSomeValue();
if(result is None)
{
}
else
{
}

Nhưng tôi nghĩ hầu hết sẽ đồng ý rằng đây là một sự khốn của lập trình hướng đối tượng và bạn không bao giờ nên làm điều đó. Vì vậy, lập trình chức năng chỉ cần thêm một cú pháp sạch hơn làm cho phong cách lập trình này có vẻ ít thô? Tôi còn thiếu gì nữa?


6
Lập trình hàm là một mô hình khác với lập trình hướng đối tượng.
Basile Starynkevitch

@BasileStarynkevitch Tôi nhận ra, nhưng có những ngôn ngữ như F # là một chút của cả hai. Câu hỏi không phải là quá nhiều về các ngôn ngữ chức năng, mà nhiều hơn về vấn đề các loại dữ liệu đại số giải quyết vấn đề gì.
Điều

7
Điều gì xảy ra khi tôi xác định class ThirdOption : Option{}và cung cấp cho bạn một new ThirdOption()nơi bạn mong đợi Somehoặc None?
amon

1
Các ngôn ngữ @amon có các loại tổng thường có cách không cho phép. Ví dụ: Haskell định nghĩa data Maybe a = Just a | Nothing(tương đương với data Option a = Some a | Nonetrong ví dụ của bạn): bạn không thể thêm trường hợp thứ ba sau hoc. Mặc dù bạn có thể sắp xếp các loại tổng mô phỏng trong C # theo cách bạn đã hiển thị, nhưng nó không phải là loại đẹp nhất.
Martijn

1
Tôi nghĩ rằng đó là "vấn đề nào làm ADT giải quyết" và giống như "ADT là một cách tiếp cận khác nhau".
Toán học,

Câu trả lời:


44

Các lớp với giao diện và kế thừa thể hiện một thế giới mở: Bất kỳ ai cũng có thể thêm một loại dữ liệu mới. Đối với một giao diện nhất định, có thể có các lớp triển khai trên toàn thế giới, trong các tệp khác nhau, trong các dự án khác nhau, tại các công ty khác nhau. Chúng giúp dễ dàng thêm các trường hợp vào cấu trúc dữ liệu, nhưng vì việc triển khai giao diện được phân cấp, nên khó có thể thêm một phương thức mới vào giao diện. Khi một giao diện được công khai, về cơ bản nó sẽ bị đóng băng. Không ai biết tất cả các triển khai có thể.

Các kiểu dữ liệu đại số là kép đối với điều đó, chúng được đóng lại . Tất cả các trường hợp của dữ liệu được liệt kê ở một nơi và các hoạt động không chỉ có thể liệt kê các biến thể một cách triệt để, chúng được khuyến khích để làm như vậy. Do đó, việc viết một hàm mới hoạt động trên một kiểu dữ liệu đại số là chuyện nhỏ: Chỉ cần viết hàm chết tiệt. Đổi lại, việc thêm các trường hợp mới rất phức tạp vì về cơ bản bạn cần phải đi qua toàn bộ cơ sở mã và mở rộng mọi trường hợp match. Tương tự như tình huống với các giao diện, trong thư viện tiêu chuẩn Rust, việc thêm một biến thể mới là một thay đổi đột phá (đối với các loại công khai).

Đây là hai mặt của vấn đề biểu hiện . Các kiểu dữ liệu đại số là một giải pháp không đầy đủ cho chúng, nhưng OOP cũng vậy. Cả hai đều có lợi thế tùy thuộc vào số lượng trường hợp có dữ liệu, tần suất các trường hợp đó thay đổi và tần suất các hoạt động được mở rộng hoặc thay đổi. (Đó là lý do tại sao nhiều ngôn ngữ hiện đại cung cấp cả hai, hoặc một cái gì đó tương tự, hoặc đi thẳng vào các cơ chế mạnh hơn và phức tạp hơn, cố gắng thực hiện cả hai cách tiếp cận.)


Bạn có nhận được một lỗi biên dịch nếu bạn không cập nhật một trận đấu, hoặc chỉ trong thời gian chạy bạn phát hiện ra?
Ian

6
@Ian Hầu hết các ngôn ngữ chức năng được gõ tĩnh và kiểm tra tính toàn diện của khớp mẫu. Tuy nhiên, nếu có một mẫu "bắt tất cả", trình biên dịch sẽ hài lòng ngay cả khi hàm sẽ phải xử lý trường hợp mới để thực hiện công việc của nó. Ngoài ra, bạn phải biên dịch lại tất cả các mã phụ thuộc, bạn không thể biên dịch chỉ một thư viện và liên kết lại nó thành một ứng dụng đã được xây dựng.

Ngoài ra, kiểm tra tĩnh rằng một mô hình là toàn diện là đắt tiền nói chung. Nó giải quyết vấn đề SAT tốt nhất và tồi tệ nhất là ngôn ngữ cho phép các vị từ tùy ý làm cho điều này trở nên không thể giải quyết được.
usr

@usr Không có ngôn ngữ nào tôi biết về việc thử một giải pháp hoàn hảo, trình biên dịch hiểu rằng nó hoàn toàn hoặc bạn buộc phải thêm một trường hợp bắt tất cả khi bạn gặp sự cố và bị cháy. Tôi không biết về mối quan hệ với SAT, bạn có liên kết đến việc giảm bớt không? Bất kể, đối với mã thực tế được viết trong các chương trình thực tế, kiểm tra mức độ cạn kiệt là một giọt trong thùng.

Hãy tưởng tượng bạn đang kết hợp trên N booleans. Sau đó, bạn thêm các mệnh đề khớp như (a, b, _, d, ...). Trường hợp bắt tất cả là sau đó! Mệnh đề 1 &&! Mệnh đề 2 && .... Điều đó giống như SAT đối với tôi.
usr

12

Vì vậy, lập trình chức năng chỉ cần thêm một cú pháp sạch hơn làm cho phong cách lập trình này có vẻ ít thô?

Đó là một sự đơn giản hóa có lẽ, nhưng có.

Tôi còn thiếu gì nữa?

Chúng ta hãy rõ ràng về các loại dữ liệu đại số là gì (tóm tắt liên kết tốt này từ Tìm hiểu bạn là Haskell):

  • Một loại tổng có "giá trị này có thể là A hoặc B".
  • Một loại sản phẩm cho biết "giá trị này là cả A B".

ví dụ của bạn chỉ thực sự hoạt động với cái đầu tiên.

Điều bạn có lẽ còn thiếu là bằng cách cung cấp hai thao tác cơ bản này, các ngôn ngữ chức năng cho phép bạn xây dựng mọi thứ khác. C # có các cấu trúc, lớp, enum, generic trên đầu trang và hàng đống quy tắc để điều chỉnh cách thức những thứ này hoạt động.

Kết hợp với một số cú pháp để trợ giúp, các ngôn ngữ chức năng có thể phân tách các hoạt động theo hai đường dẫn này, cung cấp một cách tiếp cận sạch sẽ, đơn giản và thanh lịch cho các loại.

Vấn đề nào làm các loại dữ liệu đại số giải quyết?

Họ giải quyết vấn đề tương tự như bất kỳ hệ thống loại nào khác: "những giá trị nào là hợp pháp để sử dụng ở đây?" - họ chỉ có một cách tiếp cận khác với nó.


4
Câu cuối cùng của bạn giống như nói với ai đó rằng "tàu giải quyết vấn đề tương tự như máy bay: vận tải - họ chỉ thực hiện một cách tiếp cận khác với nó". Đó là một tuyên bố hoàn toàn chính xác, và cũng là một tuyên bố khá vô dụng.
Mehrdad

@mehrdad - Tôi nghĩ đó là cường điệu hóa nó một chút.
Telastyn

1
Không còn nghi ngờ gì nữa, bạn hiểu các loại dữ liệu đại số chỉ tốt, nhưng danh sách dấu đầu dòng của bạn ("Loại tổng là ..." và "Loại sản phẩm là ...") trông giống như mô tả về loại kết hợp và loại giao nhau, không phải là mô tả khá giống với các loại tổng và sản phẩm.
pyon

6

Bạn có thể ngạc nhiên khi biết rằng khớp mẫu không được coi là cách thành ngữ nhất để làm việc với Tùy chọn. Xem tài liệu về Tùy chọn của Scala để biết thêm về điều đó. Tôi không chắc tại sao rất nhiều hướng dẫn của FP khuyến khích việc sử dụng này.

Hầu hết những gì bạn đang thiếu là có cả đống chức năng được tạo để giúp làm việc với Tùy chọn dễ dàng hơn. Xem xét ví dụ chính từ các tài liệu Scala:

val name: Option[String] = request getParameter "name"
val upper = name map { _.trim } filter { _.length != 0 } map { _.toUpperCase }
println(upper getOrElse "")

Lưu ý cách mapfiltercho phép bạn thực hiện chuỗi thao tác trên Tùy chọn mà không phải kiểm tra tại mỗi điểm xem bạn có Nonehay không. Sau đó, ở cuối, bạn sử dụng getOrElseđể chỉ định một giá trị mặc định. Không có gì bạn đang làm một cái gì đó "thô" như kiểm tra các loại. Bất kỳ kiểm tra loại không thể tránh khỏi được thực hiện nội bộ vào thư viện. Haskell có bộ chức năng hậu môn riêng, chưa kể đến bộ chức năng lớn sẽ hoạt động trên bất kỳ đơn nguyên hoặc functor nào.

Các kiểu dữ liệu đại số khác có các cách thành ngữ riêng để làm việc với chúng và kết hợp mẫu thấp ở cực totem trong hầu hết các trường hợp. Tương tự như vậy, khi bạn tạo các loại của riêng mình, bạn sẽ cung cấp các chức năng tương tự để làm việc với 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.