Hãy xem xét Functorlớp kiểu trong Haskell, đâu flà biến kiểu kiểu cao hơn:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Chữ ký kiểu này nói gì là fmap thay đổi tham số kiểu của một ftừ athành b, nhưng vẫn fgiữ nguyên như cũ. Vì vậy, nếu bạn sử dụng fmaptrên một danh sách, bạn sẽ nhận được một danh sách, nếu bạn sử dụng nó trên một trình phân tích cú pháp, bạn sẽ nhận được một trình phân tích cú pháp, v.v. Và đây là những đảm bảo tĩnh , thời gian biên dịch.
Tôi không biết F #, nhưng chúng ta hãy xem xét điều gì sẽ xảy ra nếu chúng ta cố gắng diễn đạt sự Functortrừu tượng bằng một ngôn ngữ như Java hoặc C #, với sự kế thừa và generics, nhưng không có generic cao hơn. Lần thử đầu tiên:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Vấn đề với lần thử đầu tiên này là việc triển khai giao diện được phép trả về bất kỳ lớp nào thực hiện Functor. Ai đó có thể viết một phương thức FunnyList<A> implements Functor<A>có mapphương thức trả về một loại tập hợp khác hoặc thậm chí một cái gì đó khác không phải là một tập hợp nhưng vẫn là một Functor. Ngoài ra, khi bạn sử dụng mapphương thức này, bạn không thể gọi bất kỳ phương thức cụ thể kiểu phụ nào trên kết quả trừ khi bạn chuyển nó xuống kiểu mà bạn thực sự mong đợi. Vì vậy, chúng tôi có hai vấn đề:
- Hệ thống kiểu không cho phép chúng ta thể hiện bất biến rằng
mapphương thức luôn trả về cùng một Functorlớp con với người nhận.
- Do đó, không có cách an toàn kiểu tĩnh nào để gọi một
Functorphương thức không phải là kết quả của map.
Bạn có thể thử những cách khác phức tạp hơn, nhưng không có cách nào thực sự hiệu quả. Ví dụ: bạn có thể thử tăng cường lần thử đầu tiên bằng cách xác định các kiểu phụ của kiểu Functorhạn chế kết quả:
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
Điều này giúp cấm những người triển khai các giao diện hẹp hơn đó trả về loại sai Functortừ mapphương thức, nhưng vì không có giới hạn về số lượng Functortriển khai bạn có thể có, nên không có giới hạn về số lượng giao diện hẹp hơn bạn sẽ cần.
( CHỈNH SỬA: Và lưu ý rằng điều này chỉ hoạt động vì nó Functor<B>xuất hiện dưới dạng loại kết quả và do đó, giao diện con có thể thu hẹp nó. Vì vậy AFAIK chúng tôi không thể thu hẹp cả hai cách sử dụng Monad<B>trong giao diện sau:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Trong Haskell, với các biến kiểu cấp cao hơn, đây là (>>=) :: Monad m => m a -> (a -> m b) -> m b.)
Tuy nhiên, một thử nghiệm khác là sử dụng các generic đệ quy để thử và có giao diện hạn chế kiểu kết quả của kiểu con đối với chính kiểu con đó. Ví dụ đồ chơi:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Nhưng loại kỹ thuật này (khá phức tạp đối với nhà phát triển OOP hàng đầu của bạn, còn đối với nhà phát triển chức năng hàng loạt của bạn) vẫn không thể thể hiện Functorràng buộc mong muốn :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Vấn đề ở đây là điều này không hạn chế FBcó giống Fnhư FA— vì vậy khi bạn khai báo một kiểu List<A> implements Functor<List<A>, A>, mapphương thức vẫn có thể trả về a NotAList<B> implements Functor<NotAList<B>, B>.
Lần thử cuối cùng, trong Java, sử dụng các kiểu thô (vùng chứa chưa được phân loại):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Ở đây Fsẽ được khởi tạo cho các loại chưa được phân loại như just Listhoặc Map. Điều này đảm bảo rằng a FunctorStrategy<List>chỉ có thể trả về a List—nhưng bạn đã từ bỏ việc sử dụng các biến kiểu để theo dõi các loại phần tử của danh sách.
Trọng tâm của vấn đề ở đây là các ngôn ngữ như Java và C # không cho phép các tham số kiểu có tham số. Trong Java, nếu Tlà một biến kiểu, bạn có thể viết Tvà List<T>nhưng không T<String>. Các loại thân thiện hơn loại bỏ hạn chế này, do đó bạn có thể có một cái gì đó như thế này (không được suy nghĩ đầy đủ):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
Và giải quyết vấn đề này cụ thể:
(Tôi nghĩ) Tôi hiểu rằng thay vì myList |> List.map fhoặc myList |> Seq.map f |> Seq.toListcác loại kinded cao hơn cho phép bạn chỉ cần viết myList |> map fvà nó sẽ trả về a List. Điều đó thật tuyệt (giả sử nó chính xác), nhưng có vẻ hơi nhỏ? (Và nó không thể được thực hiện đơn giản bằng cách cho phép quá tải hàm?) Tôi thường chuyển đổi thành Seqanyway và sau đó tôi có thể chuyển đổi thành bất kỳ thứ gì tôi muốn sau đó.
Có nhiều ngôn ngữ khái quát hóa ý tưởng của maphàm theo cách này, bằng cách mô hình hóa nó như thể, về cơ bản, ánh xạ là về các chuỗi. Nhận xét này của bạn là trên tinh thần đó: nếu bạn có một loại hỗ trợ chuyển đổi sang và đi Seq, bạn sẽ nhận được hoạt động bản đồ "miễn phí" bằng cách sử dụng lại Seq.map.
Tuy nhiên, trong Haskell, Functorlớp này chung chung hơn thế; nó không bị ràng buộc với khái niệm về trình tự. Bạn có thể triển khai fmapcho các loại không có ánh xạ tốt tới các chuỗi, như IOhành động, tổ hợp phân tích cú pháp, hàm, v.v.:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
Khái niệm "ánh xạ" thực sự không gắn liền với trình tự. Tốt nhất bạn nên hiểu luật của functor:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Rất chính thức:
- Luật đầu tiên nói rằng ánh xạ với một hàm nhận dạng / noop giống như không làm gì cả.
- Định luật thứ hai nói rằng bất kỳ kết quả nào bạn có thể tạo ra bằng cách ánh xạ hai lần, bạn cũng có thể tạo ra bằng cách ánh xạ một lần.
Đây là lý do tại sao bạn muốn fmapduy trì kiểu — bởi vì ngay sau khi bạn nhận được các mapphép toán tạo ra một kiểu kết quả khác, việc đảm bảo như thế này sẽ trở nên khó khăn hơn rất nhiều.