Vâng, một câu hỏi rất đơn giản trên bề mặt. Nhưng nếu bạn dành thời gian để suy nghĩ cho đến cuối cùng, bạn sẽ đi sâu vào lý thuyết loại vô cùng. Và loại lý thuyết cũng nhìn chằm chằm vào bạn.
Tất nhiên, trước tiên, bạn đã nhận ra một cách chính xác rằng F # không có các lớp loại và đó là lý do tại sao. Nhưng bạn đề xuất một giao diện Mappable
. Ok, chúng ta hãy nhìn vào đó.
Hãy nói rằng chúng ta có thể tuyên bố một giao diện như vậy. Bạn có thể tưởng tượng chữ ký của nó sẽ trông như thế nào không?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
Đâu f
là kiểu thực hiện giao diện. Đợi đã! F # cũng không có điều đó! Đây f
là một biến loại cao hơn và F # hoàn toàn không có loại tốt hơn. Không có cách nào để khai báo một chức năng f : 'm<'a> -> 'm<'b>
hoặc một cái gì đó như thế.
Nhưng ok, giả sử chúng ta cũng vượt qua rào cản đó. Và bây giờ chúng ta có một giao diện Mappable
có thể được thực hiện bởi List
, Array
, Seq
, và các bồn rửa chén. Nhưng chờ đã! Bây giờ chúng ta có một phương thức thay vì một hàm và các phương thức không được soạn thảo tốt! Hãy xem xét thêm 42 vào mọi thành phần của danh sách lồng nhau:
// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))
// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))
Hãy nhìn: bây giờ chúng ta phải sử dụng một biểu thức lambda! Không có cách nào để chuyển việc .map
thực hiện này sang một chức năng khác như một giá trị. Thực tế, kết thúc "chức năng là giá trị" (và vâng, tôi biết, sử dụng lambda trông không tệ lắm trong ví dụ này, nhưng tin tôi đi, nó trở nên rất xấu xí)
Nhưng chờ đã, chúng ta vẫn chưa xong. Bây giờ, đó là một cuộc gọi phương thức, gõ suy luận không hoạt động! Bởi vì chữ ký loại của phương thức .NET phụ thuộc vào loại đối tượng, không có cách nào để trình biên dịch suy ra cả hai. Đây thực sự là một vấn đề rất phổ biến người mới gặp phải khi tương tác với các thư viện .NET. Và cách chữa trị duy nhất là cung cấp một chữ ký loại:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
Ồ, nhưng điều này vẫn chưa đủ! Mặc dù tôi đã cung cấp một chữ ký cho nestedList
chính nó, tôi đã không cung cấp một chữ ký cho tham số của lambda l
. Chữ ký như vậy nên là gì? Bạn có thể nói nó nên được fun (l: #Mappable) -> ...
không? Ồ, và bây giờ cuối cùng chúng ta cũng đã đạt được loại N, như bạn thấy, #Mappable
là một lối tắt cho "bất kỳ loại nào 'a
như vậy 'a :> Mappable
" - tức là một biểu thức lambda tự nó là chung chung.
Hoặc, thay vào đó, chúng ta có thể quay trở lại loại tốt hơn và tuyên bố loại nestedList
chính xác hơn:
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
Nhưng ok, bây giờ chúng ta hãy bỏ qua suy luận kiểu và quay lại biểu thức lambda và làm thế nào bây giờ chúng ta không thể chuyển map
như một giá trị cho hàm khác. Giả sử chúng ta mở rộng cú pháp một chút để cho phép một cái gì đó giống như những gì Elm làm với các trường bản ghi:
add42 nestedList = nestedList.map (.map ((+) 42))
Loại .map
sẽ là gì? Nó sẽ phải là một loại hạn chế , giống như trong Haskell!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
Oh, tốt. Đặt sang một bên thực tế là .NET thậm chí không cho phép các loại như vậy tồn tại, thực sự chúng ta vừa lấy lại các lớp loại!
Nhưng có một lý do mà F # không có lớp loại ở vị trí đầu tiên. Nhiều khía cạnh của lý do đó được mô tả ở trên, nhưng một cách ngắn gọn hơn để đặt nó là: sự đơn giản .
Để bạn thấy, đây là một quả bóng sợi. Khi bạn có các lớp loại, bạn phải có các ràng buộc, loại cao hơn, hạng N (hoặc ít nhất là hạng 2) và trước khi bạn biết điều đó, bạn đang yêu cầu các kiểu bắt buộc, hàm loại, GADT và tất cả phần còn lại của nó
Nhưng Haskell phải trả giá cho tất cả các goodies. Hóa ra không có cách nào tốt để suy ra tất cả những thứ đó. Các loại sắp xếp cao hơn hoạt động, nhưng các ràng buộc đã không được. Hạng-N - thậm chí không mơ về nó. Và ngay cả khi nó hoạt động, bạn nhận được các lỗi loại mà bạn phải có bằng tiến sĩ để hiểu. Và đó là lý do tại Haskell, bạn được khuyến khích nhẹ nhàng đặt chữ ký loại lên mọi thứ. Chà, không phải tất cả mọi thứ - mọi thứ , nhưng thực sự là hầu hết mọi thứ. Và nơi bạn không đặt chữ ký loại (ví dụ: bên trong let
và where
) - ngạc nhiên - ngạc nhiên, những địa điểm đó thực sự bị biến dạng, do đó, về cơ bản bạn trở lại trong F # -land đơn giản.
Mặt khác, trong F #, chữ ký loại rất hiếm, chủ yếu chỉ dành cho tài liệu hoặc cho .NET interop. Ngoài hai trường hợp đó, bạn có thể viết cả một chương trình phức tạp lớn trong F # và không sử dụng chữ ký loại một lần. Kiểu suy luận hoạt động tốt, bởi vì không có gì quá phức tạp hoặc mơ hồ để nó xử lý.
Và đây là lợi thế lớn của F # so với Haskell. Vâng, Haskell cho phép bạn thể hiện những thứ siêu phức tạp theo một cách rất chính xác, điều đó thật tốt. Nhưng F # cho phép bạn rất mơ mộng, gần giống như Python hoặc Ruby và vẫn có trình biên dịch bắt bạn nếu bạn vấp ngã.